diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/app_example/create_exe.py spyder-3.0.2+dfsg1/app_example/create_exe.py --- spyder-2.3.8+dfsg1/app_example/create_exe.py 2015-08-24 21:09:44.000000000 +0200 +++ spyder-3.0.2+dfsg1/app_example/create_exe.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,31 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright © 2011 Pierre Raybaut -# Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) - -"""Create a stand-alone executable""" - -try: - from guidata.disthelpers import Distribution -except ImportError: - raise ImportError("This script requires guidata 1.4+") - -import spyderlib - - -def create_executable(): - """Build executable using ``guidata.disthelpers``""" - dist = Distribution() - dist.setup(name="Example", version="1.1", - description="Embedding Spyder Qt shell", - script="example.pyw", target_name="example.exe") - spyderlib.add_to_distribution(dist) - #dist.add_modules('matplotlib') # Uncomment if you need matplotlib - dist.excludes += ['IPython'] - # Building executable - dist.build('cx_Freeze') - - -if __name__ == '__main__': - create_executable() diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/app_example/example.pyw spyder-3.0.2+dfsg1/app_example/example.pyw --- spyder-2.3.8+dfsg1/app_example/example.pyw 2015-08-24 21:09:44.000000000 +0200 +++ spyder-3.0.2+dfsg1/app_example/example.pyw 1970-01-01 01:00:00.000000000 +0100 @@ -1,72 +0,0 @@ - -from spyderlib.qt.QtGui import (QApplication, QMainWindow, QWidget, QLabel, - QLineEdit, QHBoxLayout, QDockWidget, QFont) -from spyderlib.qt.QtCore import Qt - -from spyderlib.widgets.internalshell import InternalShell - - -class MyWidget(QWidget): - def __init__(self): - QWidget.__init__(self) - label = QLabel("Imagine an extraordinary complex widget right here...") - self.edit = QLineEdit("Text") - layout = QHBoxLayout() - layout.addWidget(label) - layout.addWidget(self.edit) - self.setLayout(layout) - - def get_text(self): - """Return sample edit text""" - return self.edit.text() - - def set_text(self, text): - """Set sample edit text""" - self.edit.setText(text) - - -class MyWindow(QMainWindow): - def __init__(self): - QMainWindow.__init__(self) - - # Set this very simple widget as central widget - widget = MyWidget() - self.setCentralWidget(widget) - - # Create the console widget - font = QFont("Courier new") - font.setPointSize(10) - ns = {'win': self, 'widget': widget} - msg = "Try for example: widget.set_text('foobar') or win.close()" - # Note: by default, the internal shell is multithreaded which is safer - # but not compatible with graphical user interface creation. - # For example, if you need to plot data with Matplotlib, you will need - # to pass the option: multithreaded=False - self.console = cons = InternalShell(self, namespace=ns, message=msg) - - # Setup the console widget - cons.set_font(font) - cons.set_codecompletion_auto(True) - cons.set_calltips(True) - cons.setup_calltips(size=600, font=font) - cons.setup_completion(size=(300, 180), font=font) - console_dock = QDockWidget("Console", self) - console_dock.setWidget(cons) - - # Add the console widget to window as a dockwidget - self.addDockWidget(Qt.BottomDockWidgetArea, console_dock) - - self.resize(800, 600) - - def closeEvent(self, event): - self.console.exit_interpreter() - event.accept() - -def main(): - app = QApplication([]) - win = MyWindow() - win.show() - app.exec_() - -if __name__ == "__main__": - main() \ Kein Zeilenumbruch am Dateiende. diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/bootstrap.py spyder-3.0.2+dfsg1/bootstrap.py --- spyder-2.3.8+dfsg1/bootstrap.py 2015-11-27 14:29:54.000000000 +0100 +++ spyder-3.0.2+dfsg1/bootstrap.py 2016-11-17 01:27:51.000000000 +0100 @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- # # Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) +# (see spyder/__init__.py for details) """ Bootstrapping Spyder @@ -32,7 +32,8 @@ Type `python bootstrap.py -- --help` to read about Spyder options.""") parser.add_option('--gui', default=None, - help="GUI toolkit: pyqt (for PyQt4) or pyside (for PySide)") + help="GUI toolkit: pyqt5 (for PyQt5), pyqt (for PyQt4) or " + "pyside (for PySide, deprecated)") parser.add_option('--hide-console', action='store_true', default=False, help="Hide parent console window (Windows only)") parser.add_option('--test', dest="test", action='store_true', default=False, @@ -41,9 +42,13 @@ default=False, help="Disable Apport exception hook (Ubuntu)") parser.add_option('--debug', action='store_true', default=False, help="Run Spyder in debug mode") + options, args = parser.parse_args() -assert options.gui in (None, 'pyqt', 'pyside'), \ +# Store variable to be used in self.restart (restart spyder instance) +os.environ['SPYDER_BOOTSTRAP_ARGS'] = str(sys.argv[1:]) + +assert options.gui in (None, 'pyqt5', 'pyqt', 'pyside'), \ "Invalid GUI toolkit option '%s'" % options.gui # For testing purposes @@ -92,27 +97,28 @@ # --- Continue -from spyderlib.utils.vcs import get_hg_revision -print("Revision %s:%s, Branch: %s" % get_hg_revision(DEVPATH)) +from spyder.utils.vcs import get_git_revision +print("Revision %s, Branch: %s" % get_git_revision(DEVPATH)) sys.path.insert(0, DEVPATH) print("01. Patched sys.path with %s" % DEVPATH) -EXTPATH = osp.join(DEVPATH, 'external-py' + sys.version[0]) -if osp.isdir(EXTPATH): - sys.path.insert(0, EXTPATH) - print(" and %s" % EXTPATH) - -# Selecting the GUI toolkit: PySide if installed, otherwise PyQt4 +# Selecting the GUI toolkit: PyQt5 if installed, otherwise PySide or PyQt4 # (Note: PyQt4 is still the officially supported GUI toolkit for Spyder) if options.gui is None: try: - import PySide # analysis:ignore - print("02. PySide is detected, selecting (experimental)") - os.environ['QT_API'] = 'pyside' - except: - print("02. No PySide detected, using PyQt4 if available") + import PyQt5 # analysis:ignore + print("02. PyQt5 is detected, selecting") + os.environ['QT_API'] = 'pyqt5' + except ImportError: + try: + import PyQt4 # analysis:ignore + print("02. PyQt4 is detected, selecting") + os.environ['QT_API'] = 'pyqt' + except ImportError: + print("02. No PyQt5 or PyQt4 detected, using PySide if available " + "(deprecated)") else: print ("02. Skipping GUI toolkit detection") os.environ['QT_API'] = options.gui @@ -120,7 +126,7 @@ if options.debug: # safety check - Spyder config should not be imported at this point - if "spyderlib.baseconfig" in sys.modules: + if "spyder.config.base" in sys.modules: sys.exit("ERROR: Can't enable debug mode - Spyder is already imported") print("0x. Switching debug mode on") os.environ["SPYDER_DEBUG"] = "True" @@ -130,7 +136,7 @@ # Checking versions (among other things, this has the effect of setting the # QT_API environment variable if this has not yet been done just above) -from spyderlib import get_versions +from spyder import get_versions versions = get_versions(reporev=False) print("03. Imported Spyder %s" % versions['spyder']) print(" [Python %s %dbits, Qt %s, %s %s on %s]" % \ @@ -138,14 +144,22 @@ versions['qt_api'], versions['qt_api_ver'], versions['system'])) +# Check that we have the right qtpy version +from spyder.utils import programs +if not programs.is_module_installed('qtpy', '>=1.1.0'): + print("") + sys.exit("ERROR: Your qtpy version is outdated. Please install qtpy " + "1.1.0 or higher to be able to work with Spyder!") + + # --- Executing Spyder if not options.hide_console and os.name == 'nt': print("0x. Enforcing parent console (Windows only)") sys.argv.append("--show-console") # Windows only: show parent console -print("04. Executing spyder.main()") -from spyderlib import start_app +print("04. Running Spyder") +from spyder.app import start time_lapse = time.time()-time_start print("Bootstrap completed in " + @@ -153,4 +167,4 @@ # gmtime() converts float into tuple, but loses milliseconds ("%.4f" % time_lapse).split('.')[1]) -start_app.main() +start.main() diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/debian/changelog spyder-3.0.2+dfsg1/debian/changelog --- spyder-2.3.8+dfsg1/debian/changelog 2016-02-16 15:45:31.000000000 +0100 +++ spyder-3.0.2+dfsg1/debian/changelog 2016-12-15 10:33:23.000000000 +0100 @@ -1,8 +1,65 @@ -spyder (2.3.8+dfsg1-1build1) xenial; urgency=medium +spyder (3.0.2+dfsg1-0ubuntu0.1) yakkety; urgency=medium - * No change rebuild to drop python3.4 support. + * New upstream version, enables spyder to run on yakkety + (LP: #1620300) + - New version needed since Qt4WebKit has been removed + - debian/control: drop dependency on python-nbconvert, rename + debian python{,3}-qtconsole to ipython{,3}-qtconsole - -- Dimitri John Ledkov Tue, 16 Feb 2016 14:45:31 +0000 + -- Scott Howard Thu, 15 Dec 2016 10:33:23 +0100 + +spyder (3.0.2+dfsg1-1) unstable; urgency=medium + + * New upstream version 3.0.2+dfsg1 + * Move the locale data into the spyder-common package + + -- Picca Frédéric-Emmanuel Sun, 30 Oct 2016 19:22:44 +0100 + +spyder (3.0.1+dfsg1-1) unstable; urgency=medium + + * New upstream version 3.0.1+dfsg1 + + -- Picca Frédéric-Emmanuel Sun, 30 Oct 2016 19:22:43 +0100 + +spyder (3.0.0+dfsg1-1~exp1) experimental; urgency=medium + + * New upstream version 3.0.0+dfsg1 + * Upstream renamed module spyderlib into spyder + * d/control + - New binary packages: python-spyder, python3-spyder, spyder-doc + - python-spyderlib, python3-spyderlib, python-spyder-doc are now + transitional packages. + + -- Picca Frédéric-Emmanuel Sun, 25 Sep 2016 13:21:06 +0200 + +spyder (3.0.0~b5+dfsg1-1~exp1) experimental; urgency=medium + + * Imported Upstream version 3.0.0~b5+dfsg1 + * debian/control + - Added Depends: python-pep8, python3-pep8 + + -- Picca Frédéric-Emmanuel Tue, 23 Aug 2016 22:24:06 +0200 + +spyder (3.0.0~b4+dfsg1-1~exp1) experimental; urgency=medium + + * Imported Upstream version 3.0.0~b4+dfsg1 + (Closes: #784613, #797069, #826042, #811339) + * debian/watch + - Use the Debian pypi redirector + * debian/copyright + - updated Files-Excluded + * debian/control + - update the Vcs-X fileds + - Remove Alexandre Fayolle and Ludovic Aubry from Uploaders + thanks for their work. (Closes: #833330) + - Added Depends: python-nbconvert, python-qtawesome, python-qtconsole, + python-qtpy, python-pickleshare, python-pyflakes, python-pygments, + python-zmq, + python3-nbconvert, python3-qtawesome, python3-qtconsole, + python3-qtpy, python3-pickleshare, python3-pyflakes, python3-pygments, + python3-zmq + + -- Picca Frédéric-Emmanuel Sun, 21 Aug 2016 11:52:26 +0200 spyder (2.3.8+dfsg1-1) unstable; urgency=medium diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/debian/control spyder-3.0.2+dfsg1/debian/control --- spyder-2.3.8+dfsg1/debian/control 2015-11-01 15:00:44.000000000 +0100 +++ spyder-3.0.2+dfsg1/debian/control 2016-12-15 10:33:04.000000000 +0100 @@ -1,8 +1,6 @@ Source: spyder Maintainer: Debian Science Maintainers -Uploaders: Ludovic Aubry , - Alexandre Fayolle , - Picca Frédéric-Emmanuel +Uploaders: Picca Frédéric-Emmanuel Section: science Priority: extra Build-Depends: debhelper (>= 9), @@ -11,14 +9,12 @@ python-setuptools, python-sphinx, python3-all, - python3-pyqt4, python3-setuptools, - python3-sip, python3-sphinx, xsltproc -Standards-Version: 3.9.6 -Vcs-Browser: http://anonscm.debian.org/cgit/debian-science/packages/spyder.git -Vcs-Git: git://anonscm.debian.org/debian-science/packages/spyder.git +Standards-Version: 3.9.8 +Vcs-Browser: https://anonscm.debian.org/cgit/debian-science/packages/spyder.git +Vcs-Git: https://anonscm.debian.org/git/debian-science/packages/spyder.git Homepage: https://github.com/spyder-ide/spyder X-Python-Version: >= 2.7 X-Python3-Version: >= 3.2 @@ -28,7 +24,7 @@ Section: devel Depends: ${misc:Depends}, ${python:Depends}, - python-spyderlib (= ${binary:Version}) + python-spyder (= ${binary:Version}) Breaks: ${python-Breaks}, python-spyderlib (<< 2.3.0+dfsg-2) Replaces: python-spyderlib (<< 2.3.0+dfsg-2) @@ -41,34 +37,50 @@ Package: python-spyderlib Architecture: all +Section: oldlibs +Depends: ${misc:Depends}, python-spyder +Description: transitional dummy package for python-spyder + This is a transitional package to ease upgrades to the + python-spyder package. It can be safely removed. + +Package: python-spyder +Architecture: all Section: python Depends: ${misc:Depends}, ${python:Depends}, libjs-jquery, libjs-mathjax, - python-qt4, - spyder-common -Recommends: ipython-qtconsole, - pep8, - pyflakes (>= 0.5.0), - pylint, - python-bs4, - python-jedi, - python-matplotlib, + pep8, + pylint, + python-bs4, + python-jedi, + python-pep8, + python-psutil, + python-qtawesome, + ipython-qtconsole, + python-qtpy (>= 1.1.0), + python-pickleshare, + python-pyflakes, + python-pygments, + python-rope, + python-sphinx, + python-zmq, + spyder-common (= ${binary:Version}), +Recommends: python-matplotlib, python-numpy, python-pandas, - python-psutil (>= 0.3.0), - python-rope, python-scipy, - python-sphinx, - python-spyderlib-doc (= ${binary:Version}) + spyder-doc (= ${binary:Version}) Suggests: tortoisehg, gitk Breaks: ${python:Breaks}, spyder (<< 2.3.0+dfsg-2), - python-spyderlib-doc (<< 2.3.0+dfsg-4) -Provides: ${python:Provides} -Replaces: spyder (<< 2.0.12-1) + python-spyderlib-doc (<< 2.3.0+dfsg-4), + python-spyderlib (<< 3.0.0+dfsg1-1~) +Provides: ${python:Provides}, + python-spyderlib +Replaces: spyder (<< 2.0.12-1), + python-spyderlib (<< 3.0.0+dfsg1-1~) Description: Python IDE for scientists (Python 2 modules) Originally written to design Spyder (the Scientific PYthon Development EnviRonment), the spyderlib Python library provides @@ -82,6 +94,14 @@ Package: python-spyderlib-doc Architecture: all +Section: oldlibs +Depends: ${misc:Depends}, spyder-doc +Description: transitional dummy package for python-spyder-doc + This is a transitional package to ease upgrades to the + spyder-doc package. It can be safely removed. + +Package: spyder-doc +Architecture: all Section: doc Depends: ${misc:Depends}, ${sphinxdoc:Depends} @@ -103,7 +123,7 @@ Section: devel Depends: ${misc:Depends}, ${python3:Depends}, - python3-spyderlib (= ${binary:Version}) + python3-spyder (= ${binary:Version}) Breaks: ${python3-Breaks}, python3-spyderlib (<< 2.3.0+dfsg-2) Replaces: python3-spyderlib (<< 2.3.0+dfsg-2) @@ -116,31 +136,47 @@ Package: python3-spyderlib Architecture: all +Section: oldlibs +Depends: ${misc:Depends}, python3-spyder +Description: transitional dummy package for python3-spyder + This is a transitional package to ease upgrades to the + python3-spyder package. It can be safely removed. + +Package: python3-spyder +Architecture: all Section: python Depends: ${misc:Depends}, ${python3:Depends}, libjs-jquery, libjs-mathjax, - python3-pyqt4, - spyder-common -Recommends: ipython3-qtconsole, - python3-pep8, - pyflakes (>= 0.5.0), - pylint, - python3-bs4, - python3-jedi, - python3-matplotlib, + pep8, + pylint, + python3-bs4, + python3-jedi, + python3-pep8, + python3-psutil, + python3-qtawesome, + ipython3-qtconsole, + python3-qtpy (>= 1.1.0), + python3-pickleshare, + python3-pyflakes, + python3-pygments, + python3-sphinx, + python3-zmq, + spyder-common (= ${binary:Version}), +Recommends: python3-matplotlib, python3-numpy, python3-pandas, - python3-psutil, python3-scipy, - python3-sphinx, - python-spyderlib-doc (= ${binary:Version}) + spyder-doc (= ${binary:Version}) Suggests: tortoisehg, gitk -Provides: ${python3:Provides} Breaks: ${python3:Breaks}, - spyder3 (<< 2.3.0+dfsg-2) + spyder3 (<< 2.3.0+dfsg-2), + python3-spyderlib (<< 3.0.0+dfsg1-1~) +Provides: ${python3:Provides}, + python3-spyderlib +Replaces: python3-spyderlib (<< 3.0.0+dfsg1-1~) Description: Python IDE for scientists (Python 3 modules) Originally written to design Spyder (the Scientific PYthon Development EnviRonment), the spyderlib Python library provides @@ -154,7 +190,6 @@ Package: spyder-common Architecture: all -Section: python Depends: ${misc:Depends} Provides: ${python3:Provides} Description: Python IDE for scientists (common files) diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/debian/copyright spyder-3.0.2+dfsg1/debian/copyright --- spyder-2.3.8+dfsg1/debian/copyright 2015-11-01 15:00:44.000000000 +0100 +++ spyder-3.0.2+dfsg1/debian/copyright 2016-10-30 19:22:44.000000000 +0100 @@ -1,9 +1,9 @@ -Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: Pierre Raybaut Upstream-Contact: pierre.raybaut@gmail.com Source: https://github.com/spyder-ide/spyder/releases -Files-Excluded: spyderlib/utils/inspector/js/mathjax - spyderlib/utils/inspector/js/jquery.js +Files-Excluded: spyder/utils/help/js/mathjax + spyder/utils/help/js/jquery.js Files: debian/* Copyright: © 2009-2010 Ludovic Aubry @@ -15,19 +15,11 @@ © 2015 The Spyder development team License: Expat -Files: spyderlib/utils/external/path.py -Copyright: © 2007 Jason Orendorff -License: public-domain - -Files: spyderlib/utils/external/pickleshare.py -Copyright: © Ville Vainio -License: Expat - -Files: spyderlib/utils/inspector/js/collapse_sections.js +Files: spyder/utils/help/js/collapse_sections.js Copyright: 2011-2012 by Assurance Technologies License: BSD-3-clause -Files: spyderlib/utils/introspection/module_completion.py +Files: spyder/utils/introspection/module_completion.py Copyright: 2005,2008,2009,2010,2011 The IPython Development Team 2001-2007, Fernando Perez 2001, Janko Hauser @@ -39,217 +31,212 @@ 2005, Jörgen Stenarson License: BSD-3-clause -Files: spyderlib/utils/inspector/sphinxify.py +Files: spyder/utils/help/sphinxify.py Copyright: © 2009 Tim Dumol © 2010 Carlos Cordoba License: BSD-3-clause -Files: spyderplugins/p_profiler.py spyderplugins/widgets/profilergui.py +Files: spyder_profiler/* Copyright: © 2011 Pierre Raybaut © 2011 Santiago Jaramillo License: Expat -Files: spyderlib/widgets/externalshell/inputhooks.py +Files: spyder/utils/inputhooks.py Copyright: (C) The IPython Development Team License: BSD-3-clause -Files: spyderlib/images/arredit.png - spyderlib/images/dictedit.png - spyderlib/images/none.png - spyderlib/images/not_found.png - spyderlib/images/matplotlib.png - spyderlib/images/options.svg - spyderlib/images/set_workdir.png - spyderlib/images/splash.png - spyderlib/images/spyder.svg - spyderlib/images/spyder_light.svg - spyderlib/images/whole_words.png - spyderlib/images/win_env.png - spyderlib/images/actions/hist.png - spyderlib/images/actions/window_nofullscreen.png - spyderlib/images/console/ipython_console.png - spyderlib/images/console/ipython_console_t.png - spyderlib/images/editor/filelist.png - spyderlib/images/editor/function.png - spyderlib/images/editor/method.png - spyderlib/images/editor/private1.png - spyderlib/images/editor/private2.png - spyderlib/images/editor/cell.png - spyderlib/images/editor/blockcomment.png - spyderlib/images/editor/refactor.png +Files: spyder/images/arredit.png + spyder/images/dictedit.png + spyder/images/none.png + spyder/images/not_found.png + spyder/images/matplotlib.png + spyder/images/options.svg + spyder/images/set_workdir.png + spyder/images/spyder.svg + spyder/images/whole_words.png + spyder/images/win_env.png + spyder/images/actions/hist.png + spyder/images/actions/window_nofullscreen.png + spyder/images/console/ipython_console.png + spyder/images/console/ipython_console_t.png + spyder/images/editor/filelist.png + spyder/images/editor/function.png + spyder/images/editor/method.png + spyder/images/editor/private1.png + spyder/images/editor/private2.png + spyder/images/editor/cell.png + spyder/images/editor/blockcomment.png + spyder/images/editor/refactor.png Copyright: © 2009-2013 Pierre Raybaut License: SLA -Files: spyderlib/images/advanced.png - spyderlib/images/arrow.png - spyderlib/images/bold.png - spyderlib/images/browser.png - spyderlib/images/font.png - spyderlib/images/genprefs.png - spyderlib/images/inspector.png - spyderlib/images/italic.png - spyderlib/images/pythonpath_mgr.png - spyderlib/images/upper_lower.png - spyderlib/images/vcs_browse.png - spyderlib/images/vcs_commit.png - spyderlib/images/actions/1downarrow.png - spyderlib/images/actions/1uparrow.png - spyderlib/images/actions/2downarrow.png - spyderlib/images/actions/2uparrow.png - spyderlib/images/actions/imshow.png - spyderlib/images/actions/insert.png - spyderlib/images/actions/lock.png - spyderlib/images/actions/lock_open.png - spyderlib/images/actions/magnifier.png - spyderlib/images/actions/next.png - spyderlib/images/actions/options_less.png - spyderlib/images/actions/options_more.png - spyderlib/images/actions/plot.png - spyderlib/images/actions/previous.png - spyderlib/images/actions/redo.png - spyderlib/images/actions/reload.png - spyderlib/images/actions/rename.png - spyderlib/images/actions/replace.png - spyderlib/images/actions/show.png - spyderlib/images/actions/special_paste.png - spyderlib/images/actions/synchronize.png - spyderlib/images/actions/tooloptions.png - spyderlib/images/actions/undo.png - spyderlib/images/actions/up.png - spyderlib/images/actions/zoom_in.png - spyderlib/images/actions/zoom_out.png - spyderlib/images/console/clear.png - spyderlib/images/console/cmdprompt_t.png - spyderlib/images/console/console.png - spyderlib/images/console/environ.png - spyderlib/images/console/history.png - spyderlib/images/console/history24.png - spyderlib/images/console/kill.png - spyderlib/images/console/prompt.png - spyderlib/images/console/restart.png - spyderlib/images/console/syspath.png - spyderlib/images/editor/bug.png - spyderlib/images/editor/close_panel.png - spyderlib/images/editor/comment.png - spyderlib/images/editor/error.png - spyderlib/images/editor/file.png - spyderlib/images/editor/fromcursor.png - spyderlib/images/editor/gotoline.png - spyderlib/images/editor/highlight.png - spyderlib/images/editor/horsplit.png - spyderlib/images/editor/indent.png - spyderlib/images/editor/last_edit_location.png - spyderlib/images/editor/newwindow.png - spyderlib/images/editor/next_cursor.png - spyderlib/images/editor/next_wng.png - spyderlib/images/editor/outline_explorer.png - spyderlib/images/editor/outline_explorer_vis.png - spyderlib/images/editor/prev_cursor.png - spyderlib/images/editor/prev_wng.png - spyderlib/images/editor/select.png - spyderlib/images/editor/selectall.png - spyderlib/images/editor/todo_list.png - spyderlib/images/editor/uncomment.png - spyderlib/images/editor/unindent.png - spyderlib/images/editor/versplit.png - spyderlib/images/editor/wng_list.png - spyderlib/images/file/fileclose.png - spyderlib/images/file/filecloseall.png - spyderlib/images/file/fileimport.png - spyderlib/images/file/filenew.png - spyderlib/images/file/fileopen.png - spyderlib/images/file/filesave.png - spyderlib/images/file/filesaveas.png - spyderlib/images/file/print.png - spyderlib/images/file/save_all.png - spyderlib/images/filetypes/bat.png - spyderlib/images/filetypes/bmp.png - spyderlib/images/filetypes/c.png - spyderlib/images/filetypes/cc.png - spyderlib/images/filetypes/cfg.png - spyderlib/images/filetypes/chm.png - spyderlib/images/filetypes/cl.png - spyderlib/images/filetypes/cmd.png - spyderlib/images/filetypes/cpp.png - spyderlib/images/filetypes/css.png - spyderlib/images/filetypes/cxx.png - spyderlib/images/filetypes/diff.png - spyderlib/images/filetypes/doc.png - spyderlib/images/filetypes/exe.png - spyderlib/images/filetypes/f.png - spyderlib/images/filetypes/f77.png - spyderlib/images/filetypes/f90.png - spyderlib/images/filetypes/gif.png - spyderlib/images/filetypes/h.png - spyderlib/images/filetypes/hh.png - spyderlib/images/filetypes/hpp.png - spyderlib/images/filetypes/htm.png - spyderlib/images/filetypes/html.png - spyderlib/images/filetypes/hxx.png - spyderlib/images/filetypes/inf.png - spyderlib/images/filetypes/ini.png - spyderlib/images/filetypes/jpeg.png - spyderlib/images/filetypes/jpg.png - spyderlib/images/filetypes/js.png - spyderlib/images/filetypes/log.png - spyderlib/images/filetypes/nt.png - spyderlib/images/filetypes/patch.png - spyderlib/images/filetypes/pdf.png - spyderlib/images/filetypes/png.png - spyderlib/images/filetypes/pps.png - spyderlib/images/filetypes/properties.png - spyderlib/images/filetypes/ps.png - spyderlib/images/filetypes/pxd.png - spyderlib/images/filetypes/pxi.png - spyderlib/images/filetypes/pyx.png - spyderlib/images/filetypes/rar.png - spyderlib/images/filetypes/readme.png - spyderlib/images/filetypes/reg.png - spyderlib/images/filetypes/rej.png - spyderlib/images/filetypes/session.png - spyderlib/images/filetypes/tar.png - spyderlib/images/filetypes/tex.png - spyderlib/images/filetypes/tgz.png - spyderlib/images/filetypes/tif.png - spyderlib/images/filetypes/tiff.png - spyderlib/images/filetypes/txt.png - spyderlib/images/filetypes/xls.png - spyderlib/images/filetypes/xml.png - spyderlib/images/filetypes/zip.png - spyderlib/images/projects/add_to_path.png - spyderlib/images/projects/folder.png - spyderlib/images/projects/package.png - spyderlib/images/projects/pp_folder.png - spyderlib/images/projects/pp_package.png - spyderlib/images/projects/pp_project.png - spyderlib/images/projects/project.png - spyderlib/images/projects/project_closed.png - spyderlib/images/projects/pythonpath.png - spyderlib/images/projects/remove_from_path.png - spyderlib/images/projects/show_all.png +Files: spyder/images/advanced.png + spyder/images/arrow.png + spyder/images/bold.png + spyder/images/browser.png + spyder/images/font.png + spyder/images/genprefs.png + spyder/images/italic.png + spyder/images/upper_lower.png + spyder/images/vcs_browse.png + spyder/images/vcs_commit.png + spyder/images/actions/1downarrow.png + spyder/images/actions/1uparrow.png + spyder/images/actions/2downarrow.png + spyder/images/actions/2uparrow.png + spyder/images/actions/imshow.png + spyder/images/actions/insert.png + spyder/images/actions/lock.png + spyder/images/actions/lock_open.png + spyder/images/actions/magnifier.png + spyder/images/actions/next.png + spyder/images/actions/options_less.png + spyder/images/actions/options_more.png + spyder/images/actions/plot.png + spyder/images/actions/previous.png + spyder/images/actions/redo.png + spyder/images/actions/reload.png + spyder/images/actions/rename.png + spyder/images/actions/replace.png + spyder/images/actions/show.png + spyder/images/actions/special_paste.png + spyder/images/actions/synchronize.png + spyder/images/actions/tooloptions.png + spyder/images/actions/undo.png + spyder/images/actions/up.png + spyder/images/actions/zoom_in.png + spyder/images/actions/zoom_out.png + spyder/images/console/cmdprompt_t.png + spyder/images/console/console.png + spyder/images/console/environ.png + spyder/images/console/history.png + spyder/images/console/history24.png + spyder/images/console/kill.png + spyder/images/console/prompt.png + spyder/images/console/restart.png + spyder/images/console/syspath.png + spyder/images/editor/bug.png + spyder/images/editor/close_panel.png + spyder/images/editor/comment.png + spyder/images/editor/error.png + spyder/images/editor/file.png + spyder/images/editor/fromcursor.png + spyder/images/editor/gotoline.png + spyder/images/editor/highlight.png + spyder/images/editor/horsplit.png + spyder/images/editor/indent.png + spyder/images/editor/last_edit_location.png + spyder/images/editor/newwindow.png + spyder/images/editor/next_cursor.png + spyder/images/editor/next_wng.png + spyder/images/editor/outline_explorer.png + spyder/images/editor/outline_explorer_vis.png + spyder/images/editor/prev_cursor.png + spyder/images/editor/prev_wng.png + spyder/images/editor/select.png + spyder/images/editor/selectall.png + spyder/images/editor/todo_list.png + spyder/images/editor/uncomment.png + spyder/images/editor/unindent.png + spyder/images/editor/versplit.png + spyder/images/editor/wng_list.png + spyder/images/file/fileclose.png + spyder/images/file/filecloseall.png + spyder/images/file/fileimport.png + spyder/images/file/filenew.png + spyder/images/file/fileopen.png + spyder/images/file/filesave.png + spyder/images/file/filesaveas.png + spyder/images/file/print.png + spyder/images/file/save_all.png + spyder/images/filetypes/bat.png + spyder/images/filetypes/bmp.png + spyder/images/filetypes/c.png + spyder/images/filetypes/cc.png + spyder/images/filetypes/cfg.png + spyder/images/filetypes/chm.png + spyder/images/filetypes/cl.png + spyder/images/filetypes/cmd.png + spyder/images/filetypes/cpp.png + spyder/images/filetypes/css.png + spyder/images/filetypes/cxx.png + spyder/images/filetypes/diff.png + spyder/images/filetypes/doc.png + spyder/images/filetypes/exe.png + spyder/images/filetypes/f.png + spyder/images/filetypes/f77.png + spyder/images/filetypes/f90.png + spyder/images/filetypes/gif.png + spyder/images/filetypes/h.png + spyder/images/filetypes/hh.png + spyder/images/filetypes/hpp.png + spyder/images/filetypes/htm.png + spyder/images/filetypes/html.png + spyder/images/filetypes/hxx.png + spyder/images/filetypes/inf.png + spyder/images/filetypes/ini.png + spyder/images/filetypes/jpeg.png + spyder/images/filetypes/jpg.png + spyder/images/filetypes/js.png + spyder/images/filetypes/log.png + spyder/images/filetypes/nt.png + spyder/images/filetypes/patch.png + spyder/images/filetypes/pdf.png + spyder/images/filetypes/png.png + spyder/images/filetypes/pps.png + spyder/images/filetypes/properties.png + spyder/images/filetypes/ps.png + spyder/images/filetypes/pxd.png + spyder/images/filetypes/pxi.png + spyder/images/filetypes/pyx.png + spyder/images/filetypes/rar.png + spyder/images/filetypes/readme.png + spyder/images/filetypes/reg.png + spyder/images/filetypes/rej.png + spyder/images/filetypes/session.png + spyder/images/filetypes/tar.png + spyder/images/filetypes/tex.png + spyder/images/filetypes/tgz.png + spyder/images/filetypes/tif.png + spyder/images/filetypes/tiff.png + spyder/images/filetypes/txt.png + spyder/images/filetypes/xls.png + spyder/images/filetypes/xml.png + spyder/images/filetypes/zip.png + spyder/images/projects/add_to_path.png + spyder/images/projects/folder.png + spyder/images/projects/package.png + spyder/images/projects/pp_folder.png + spyder/images/projects/pp_package.png + spyder/images/projects/pp_project.png + spyder/images/projects/project.png + spyder/images/projects/project_closed.png + spyder/images/projects/pythonpath.png + spyder/images/projects/remove_from_path.png + spyder/images/projects/show_all.png Copyright: © 2006-2007 Everaldo Coelho, Crystal Project License: LGPL-2 -Files: spyderlib/images/qt.png - spyderlib/images/qtassistant.png - spyderlib/images/qtdesigner.png - spyderlib/images/qtlinguist.png - spyderlib/images/filetypes/ts.png - spyderlib/images/filetypes/ui.png +Files: spyder/images/qt.png + spyder/images/qtassistant.png + spyder/images/qtdesigner.png + spyder/images/qtlinguist.png + spyder/images/filetypes/ts.png + spyder/images/filetypes/ui.png Copyright: © 2008-2009 Nokia Corporation and/or its subsidiary(-ies). © 1994-2008 Trolltech ASA. License: LGPL-2.1 -Files: spyderlib/images/pythonxy.png - spyderlib/images/vitables.png +Files: spyder/images/pythonxy.png + spyder/images/vitables.png Copyright: © 2011 Pierre Raybaut License: GPL-3 -Files: spyderlib/images/winpython.svg +Files: spyder/images/winpython.svg Copyright: © 2011 Pierre Raybaut License: Expat -Files: spyderlib/images/scipy.png +Files: spyder/images/scipy.png Copyright: 1999-2005 Travis Oliphant 2001-2002 Enthought, Inc. 2002 Eric Jones @@ -274,69 +261,66 @@ 2009 Yosef Meller License: BSD-3-clause -Files: spyderlib/images/actions/collapse.png - spyderlib/images/actions/collapse_selection.png - spyderlib/images/actions/expand.png - spyderlib/images/actions/expand_selection.png - spyderlib/images/actions/restore.png - spyderlib/images/editor/class.png - spyderlib/images/editor/convention.png - spyderlib/images/editor/todo.png - spyderlib/images/editor/warning.png - spyderlib/images/filetypes/po.png - spyderlib/images/filetypes/pot.png +Files: spyder/images/actions/collapse.png + spyder/images/actions/collapse_selection.png + spyder/images/actions/expand.png + spyder/images/actions/expand_selection.png + spyder/images/actions/restore.png + spyder/images/editor/class.png + spyder/images/editor/convention.png + spyder/images/editor/todo.png + spyder/images/editor/warning.png + spyder/images/filetypes/po.png + spyder/images/filetypes/pot.png Copyright: © 2013 FamFamFamSilk icon set 1.3 Mark James License: CC-BY -Files: spyderlib/images/actions/arrow-continue.png - spyderlib/images/actions/arrow-step-in.png - spyderlib/images/actions/arrow-step-out.png - spyderlib/images/actions/arrow-step-over.png - spyderlib/images/actions/maximize.png - spyderlib/images/actions/stop_debug.png - spyderlib/images/actions/unmaximize.png - spyderlib/images/actions/window_fullscreen.png - spyderlib/images/editor/breakpoint_big.png - spyderlib/images/editor/breakpoint_cond_big.png - spyderlib/images/editor/breakpoint_cond_small.png - spyderlib/images/editor/breakpoint_small.png - spyderlib/images/editor/debug.png - spyderlib/images/console/terminated.png - spyderlib/images/actions/stop.png +Files: spyder/images/actions/arrow-continue.png + spyder/images/actions/arrow-step-in.png + spyder/images/actions/arrow-step-out.png + spyder/images/actions/arrow-step-over.png + spyder/images/actions/maximize.png + spyder/images/actions/stop_debug.png + spyder/images/actions/unmaximize.png + spyder/images/actions/window_fullscreen.png + spyder/images/editor/breakpoint_big.png + spyder/images/editor/breakpoint_cond_big.png + spyder/images/editor/breakpoint_cond_small.png + spyder/images/editor/breakpoint_small.png + spyder/images/editor/debug.png + spyder/images/console/terminated.png + spyder/images/actions/stop.png Copyright: (C) Yusuke Kamiyamane Icons License: CC-BY - FIXME -Files: - spyderlib/images/editor/run.png - spyderlib/images/editor/run_again.png - spyderlib/images/editor/run_cell.png - spyderlib/images/editor/run_cell_advance.png - spyderlib/images/editor/run_selection.png - spyderlib/images/editor/run_settings.png - spyderlib/images/console/run_small.png +Files: spyder/images/editor/run.png + spyder/images/editor/run_again.png + spyder/images/editor/run_cell.png + spyder/images/editor/run_cell_advance.png + spyder/images/editor/run_selection.png + spyder/images/editor/run_settings.png + spyder/images/console/run_small.png Copyright: (C) The Oxygen icon theme License: CC-BY - FIXME -Files: spyderlib/images/console/python.png - spyderlib/images/console/python_t.png - spyderlib/images/filetypes/py.png - spyderlib/images/filetypes/pyc.png - spyderlib/images/filetypes/pyw.png +Files: spyder/images/console/python.png + spyder/images/console/python_t.png + spyder/images/filetypes/py.png + spyder/images/filetypes/pyc.png + spyder/images/filetypes/pyw.png Copyright: © 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010 Python Software Foundation License: PSFL -Files: spyderlib/images/filetypes/nsh.png - spyderlib/images/filetypes/nsi.png +Files: spyder/images/filetypes/nsh.png + spyder/images/filetypes/nsi.png Copyright: © 2013, NullSoft License: zlib -Files: spyderlib/images/projects/pydev.png +Files: spyder/images/projects/pydev.png Copyright: © 2013, Red Hat, Inc. and other License: EPL-1 -Files: spyderlib/widgets/dataframeeditor.py +Files: spyder/widgets/variableexplorer/dataframeeditor.py Copyright: © 2014 Spyder development team License: BSD-3-clause @@ -554,7 +538,7 @@ Free Software Foundation; version 2 of the License. . On Debian systems, the complete text of version 2 of the GNU Library - Public License can be found in `/usr/share/common-licenses/LGPL-2'. + General Public License can be found in `/usr/share/common-licenses/LGPL-2'. License: LGPL-2.1 This program is free software; you can redistribute it and/or modify it @@ -562,7 +546,7 @@ Free Software Foundation; version 2.1 of the License. . On Debian systems, the complete text of version 2.1 of the GNU Lesser - Public License can be found in `/usr/share/common-licenses/LGPL-2.1'. + General Public License can be found in `/usr/share/common-licenses/LGPL-2.1'. License: PSFL A. HISTORY OF THE SOFTWARE @@ -860,18 +844,6 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE -License: public-domain - This was created by unknown people across unknown aeons, and - changed and updated by lots of more people. It was sung by - medieval bards, and touched by that Shakespeare dude, and - possibly even Mary Queen of Scots. Who knows? Nobody knows, - not even the Shadow knows! There is no evil lurking in the - minds of anyone here. - . - This work is therefore not copyrighted by anyone, and is as - much in the public domain as anything can be. Truly, utterly, - totally. Be happy. - License: zlib The zlib/libpng License . @@ -886,4 +858,3 @@ 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. . 3. This notice may not be removed or altered from any source distribution. - diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/debian/gbp.conf spyder-3.0.2+dfsg1/debian/gbp.conf --- spyder-2.3.8+dfsg1/debian/gbp.conf 2015-11-01 15:00:44.000000000 +0100 +++ spyder-3.0.2+dfsg1/debian/gbp.conf 2016-10-30 19:22:44.000000000 +0100 @@ -1,2 +1,3 @@ [DEFAULT] debian-branch=master +upstream-branch=upstream \ Kein Zeilenumbruch am Dateiende. diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/debian/patches/0001-fix-spyderlib-path.patch spyder-3.0.2+dfsg1/debian/patches/0001-fix-spyderlib-path.patch --- spyder-2.3.8+dfsg1/debian/patches/0001-fix-spyderlib-path.patch 2015-11-01 15:00:44.000000000 +0100 +++ spyder-3.0.2+dfsg1/debian/patches/0001-fix-spyderlib-path.patch 2016-10-30 19:22:44.000000000 +0100 @@ -7,16 +7,17 @@ spyderlib/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) -diff --git a/spyderlib/__init__.py b/spyderlib/__init__.py +diff --git a/spyder/__init__.py b/spyder/__init__.py index 46da56e..cf4b33a 100644 ---- a/spyderlib/__init__.py -+++ b/spyderlib/__init__.py -@@ -35,6 +35,10 @@ __forum_url__ = 'http://groups.google.com/group/spyderlib' +--- a/spyder/__init__.py ++++ b/spyder/__init__.py +@@ -35,6 +35,11 @@ __forum_url__ = 'http://groups.google.com/group/spyderlib' # Dear (Debian, RPM, ...) package makers, please feel free to customize the # following path to module's data (images) and translations: DATAPATH = LOCALEPATH = DOCPATH = MATHJAXPATH = JQUERYPATH = '' -+DATAPATH = '/usr/share/spyderlib/images' -+DOCPATH = '/usr/share/doc/python-spyderlib-doc/html' ++DATAPATH = '/usr/share/spyder/images' ++LOCALEPATH = '/usr/share/spyder/locale' ++DOCPATH = '/usr/share/doc/spyder-doc/html' +MATHJAXPATH = '/usr/share/javascript/mathjax' +JQUERYPATH = '/usr/share/javascript/jquery' diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/debian/python3-spyder.bug-control spyder-3.0.2+dfsg1/debian/python3-spyder.bug-control --- spyder-2.3.8+dfsg1/debian/python3-spyder.bug-control 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/debian/python3-spyder.bug-control 2016-10-30 19:22:44.000000000 +0100 @@ -0,0 +1 @@ +Report-with: python3-pyqt5 diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/debian/python3-spyderlib.bug-control spyder-3.0.2+dfsg1/debian/python3-spyderlib.bug-control --- spyder-2.3.8+dfsg1/debian/python3-spyderlib.bug-control 2015-11-01 15:00:44.000000000 +0100 +++ spyder-3.0.2+dfsg1/debian/python3-spyderlib.bug-control 1970-01-01 01:00:00.000000000 +0100 @@ -1 +0,0 @@ -Report-with: python3-qt4 diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/debian/python3-spyderlib.pyremove spyder-3.0.2+dfsg1/debian/python3-spyderlib.pyremove --- spyder-2.3.8+dfsg1/debian/python3-spyderlib.pyremove 2015-11-01 15:00:44.000000000 +0100 +++ spyder-3.0.2+dfsg1/debian/python3-spyderlib.pyremove 1970-01-01 01:00:00.000000000 +0100 @@ -1,2 +0,0 @@ -/spyderlib/images/* -/spyderplugins/images/* diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/debian/python3-spyder.pyremove spyder-3.0.2+dfsg1/debian/python3-spyder.pyremove --- spyder-2.3.8+dfsg1/debian/python3-spyder.pyremove 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/debian/python3-spyder.pyremove 2016-10-30 19:22:44.000000000 +0100 @@ -0,0 +1,2 @@ +spyder/images +spyder/locale diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/debian/python-spyder.bug-control spyder-3.0.2+dfsg1/debian/python-spyder.bug-control --- spyder-2.3.8+dfsg1/debian/python-spyder.bug-control 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/debian/python-spyder.bug-control 2016-10-30 19:22:44.000000000 +0100 @@ -0,0 +1 @@ +Report-with: python-pyqt5 diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/debian/python-spyderlib.bug-control spyder-3.0.2+dfsg1/debian/python-spyderlib.bug-control --- spyder-2.3.8+dfsg1/debian/python-spyderlib.bug-control 2015-11-01 15:00:44.000000000 +0100 +++ spyder-3.0.2+dfsg1/debian/python-spyderlib.bug-control 1970-01-01 01:00:00.000000000 +0100 @@ -1 +0,0 @@ -Report-with: python-qt4 diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/debian/python-spyderlib-doc.doc-base spyder-3.0.2+dfsg1/debian/python-spyderlib-doc.doc-base --- spyder-2.3.8+dfsg1/debian/python-spyderlib-doc.doc-base 2015-11-01 15:00:44.000000000 +0100 +++ spyder-3.0.2+dfsg1/debian/python-spyderlib-doc.doc-base 1970-01-01 01:00:00.000000000 +0100 @@ -1,12 +0,0 @@ -Document: python-spyderlib-manual -Title: spyderlib documentation manual -Author: pierre.raybaut@cea.fr -Abstract: python-spyderlib_ - Spyder (previously known as Pydee) is a free open-source Python - development environment providing MATLAB-like features in a simple - and light-weighted software. -Section: Programming/Python - -Format: HTML -Index: /usr/share/doc/python-spyderlib-doc/html/index.html -Files: /usr/share/doc/python-spyderlib-doc/html/* diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/debian/python-spyderlib-doc.docs spyder-3.0.2+dfsg1/debian/python-spyderlib-doc.docs --- spyder-2.3.8+dfsg1/debian/python-spyderlib-doc.docs 2015-11-01 15:00:44.000000000 +0100 +++ spyder-3.0.2+dfsg1/debian/python-spyderlib-doc.docs 1970-01-01 01:00:00.000000000 +0100 @@ -1 +0,0 @@ -build/html \ Kein Zeilenumbruch am Dateiende. diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/debian/python-spyderlib.preinst spyder-3.0.2+dfsg1/debian/python-spyderlib.preinst --- spyder-2.3.8+dfsg1/debian/python-spyderlib.preinst 2015-11-01 15:00:44.000000000 +0100 +++ spyder-3.0.2+dfsg1/debian/python-spyderlib.preinst 1970-01-01 01:00:00.000000000 +0100 @@ -1,11 +0,0 @@ -#!/bin/sh - -set -e - -if [ "$1" = "upgrade" ] && dpkg --compare-versions "$2" lt-nl 2.1.0-1; then - if [ -L /usr/share/doc/python-spyderlib/html ]; then - rm -f /usr/share/doc/python-spyderlib/html - fi -fi - -#DEBHELPER# \ Kein Zeilenumbruch am Dateiende. diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/debian/python-spyderlib.pyremove spyder-3.0.2+dfsg1/debian/python-spyderlib.pyremove --- spyder-2.3.8+dfsg1/debian/python-spyderlib.pyremove 2015-11-01 15:00:44.000000000 +0100 +++ spyder-3.0.2+dfsg1/debian/python-spyderlib.pyremove 1970-01-01 01:00:00.000000000 +0100 @@ -1,2 +0,0 @@ -spyderlib/images -spyderplugins/images diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/debian/python-spyder.preinst spyder-3.0.2+dfsg1/debian/python-spyder.preinst --- spyder-2.3.8+dfsg1/debian/python-spyder.preinst 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/debian/python-spyder.preinst 2016-10-30 19:22:44.000000000 +0100 @@ -0,0 +1,11 @@ +#!/bin/sh + +set -e + +if [ "$1" = "upgrade" ] && dpkg --compare-versions "$2" lt-nl 2.1.0-1; then + if [ -L /usr/share/doc/python-spyderlib/html ]; then + rm -f /usr/share/doc/python-spyderlib/html + fi +fi + +#DEBHELPER# \ Kein Zeilenumbruch am Dateiende. diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/debian/python-spyder.pyremove spyder-3.0.2+dfsg1/debian/python-spyder.pyremove --- spyder-2.3.8+dfsg1/debian/python-spyder.pyremove 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/debian/python-spyder.pyremove 2016-10-30 19:22:44.000000000 +0100 @@ -0,0 +1,2 @@ +spyder/images +spyder/locale \ Kein Zeilenumbruch am Dateiende. diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/debian/rules spyder-3.0.2+dfsg1/debian/rules --- spyder-2.3.8+dfsg1/debian/rules 2015-11-01 15:00:44.000000000 +0100 +++ spyder-3.0.2+dfsg1/debian/rules 2016-10-30 19:22:44.000000000 +0100 @@ -1,14 +1,13 @@ #!/usr/bin/make -f # -*- makefile -*- -# used only until dh_python3 pyremove is backported for wheezy -PY3VERS := $(shell py3versions -vr) - # Uncomment this to turn on verbose mode. #export DH_VERBOSE=1 -export PYBUILD_NAME=spyderlib +export PYBUILD_NAME=spyder export PYBUILD_BUILD_ARGS=--no-doc export PYBUILD_INSTALL_ARGS=--no-doc +export PYBUILD_AFTER_INSTALL=rm -rf {destdir}/usr/bin/ {destdir}/usr/share + %: dh $@ --with python2,python3,sphinxdoc --buildsystem=pybuild @@ -20,27 +19,20 @@ override_dh_auto_install: dh_auto_install + # install the spyder-common files - dh_install -p spyder-common spyderlib/images usr/share/spyderlib - dh_install -p spyder-common spyderplugins/images usr/share/spyderplugins + dh_install -p spyder-common spyder/images usr/share/spyder + dh_install -p spyder-common spyder/locale usr/share/spyder + # install the spyder files - dh_install --sourcedir=$(CURDIR)/debian/python-spyderlib -p spyder usr/bin - dh_install --sourcedir=$(CURDIR)/debian/python-spyderlib -p spyder usr/share/applications/spyder.desktop - dh_install --sourcedir=$(CURDIR)/debian/python-spyderlib -p spyder usr/share/pixmaps/spyder.png - rm -rf $(CURDIR)/debian/python-spyderlib/usr/bin - rm -rf $(CURDIR)/debian/python-spyderlib/usr/share/applications - rm -rf $(CURDIR)/debian/python-spyderlib/usr/share/pixmaps - rm -f $(CURDIR)/debian/spyder/usr/bin/*.py + python setup.py install_scripts -d debian/spyder/usr/bin + python setup.py install_data -d debian/spyder/usr + rm -f debian/spyder/usr/bin/spyder_win_post_install.py + # install the spyder3 files - dh_install --sourcedir=$(CURDIR)/debian/python3-spyderlib -p spyder3 usr/bin - dh_install --sourcedir=$(CURDIR)/debian/python3-spyderlib -p spyder3 usr/share/applications/spyder3.desktop - dh_install --sourcedir=$(CURDIR)/debian/python3-spyderlib -p spyder3 usr/share/pixmaps/spyder3.png - rm -rf $(CURDIR)/debian/python3-spyderlib/usr/bin - rm -rf $(CURDIR)/debian/python3-spyderlib/usr/share/applications - rm -rf $(CURDIR)/debian/python3-spyderlib/usr/share/pixmaps - rm -f $(CURDIR)/debian/spyder3/usr/bin/*.py - # remove unwanted files until dh_python3 pyremove is backported to wheezy - for v in $(PY3VERS); do \ - rm -rf $(CURDIR)/debian/python3-spyderlib/usr/lib/python$$v/dist-packages/spyderlib/images ;\ - rm -rf $(CURDIR)/debian/python3-spyderlib/usr/lib/python$$v/dist-packages/spyderplugins/images ;\ - done + python3 setup.py install_scripts -d debian/spyder3/usr/bin + python3 setup.py install_data -d debian/spyder3/usr + rm -f debian/spyder3/usr/bin/spyder_win_post_install.py + +# skip tests for now +override_dh_auto_test: diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/debian/spyder3.bug-control spyder-3.0.2+dfsg1/debian/spyder3.bug-control --- spyder-2.3.8+dfsg1/debian/spyder3.bug-control 2015-11-01 15:00:44.000000000 +0100 +++ spyder-3.0.2+dfsg1/debian/spyder3.bug-control 2016-10-30 19:22:44.000000000 +0100 @@ -1 +1 @@ -Report-with: python3-spyderlib python3-qt4 +Report-with: python3-spyder python3-pyqt5 diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/debian/spyder3.links spyder-3.0.2+dfsg1/debian/spyder3.links --- spyder-2.3.8+dfsg1/debian/spyder3.links 2015-11-01 15:00:44.000000000 +0100 +++ spyder-3.0.2+dfsg1/debian/spyder3.links 2016-10-30 19:22:44.000000000 +0100 @@ -1 +1 @@ -/usr/share/doc/python-spyderlib-doc/html /usr/share/doc/spyder3/html +/usr/share/doc/spyder-doc/html /usr/share/doc/spyder3/html diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/debian/spyder3.menu spyder-3.0.2+dfsg1/debian/spyder3.menu --- spyder-2.3.8+dfsg1/debian/spyder3.menu 2015-11-01 15:00:44.000000000 +0100 +++ spyder-3.0.2+dfsg1/debian/spyder3.menu 1970-01-01 01:00:00.000000000 +0100 @@ -1,2 +0,0 @@ -?package(spyder3):needs="X11" section="Applications/Programming"\ - title="spyder3" command="/usr/bin/spyder3" diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/debian/spyder.bug-control spyder-3.0.2+dfsg1/debian/spyder.bug-control --- spyder-2.3.8+dfsg1/debian/spyder.bug-control 2015-11-01 15:00:44.000000000 +0100 +++ spyder-3.0.2+dfsg1/debian/spyder.bug-control 2016-10-30 19:22:44.000000000 +0100 @@ -1 +1 @@ -Report-with: python-spyderlib python-qt4 +Report-with: python-spyder python-pyqt5 diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/debian/spyder-doc.doc-base spyder-3.0.2+dfsg1/debian/spyder-doc.doc-base --- spyder-2.3.8+dfsg1/debian/spyder-doc.doc-base 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/debian/spyder-doc.doc-base 2016-10-30 19:22:44.000000000 +0100 @@ -0,0 +1,12 @@ +Document: spyder-manual +Title: spyder documentation manual +Author: pierre.raybaut@cea.fr +Abstract: spyder_ + Spyder (previously known as Pydee) is a free open-source Python + development environment providing MATLAB-like features in a simple + and light-weighted software. +Section: Programming/Python + +Format: HTML +Index: /usr/share/doc/spyder-doc/html/index.html +Files: /usr/share/doc/spyder-doc/html/* diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/debian/spyder-doc.docs spyder-3.0.2+dfsg1/debian/spyder-doc.docs --- spyder-2.3.8+dfsg1/debian/spyder-doc.docs 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/debian/spyder-doc.docs 2016-10-30 19:22:44.000000000 +0100 @@ -0,0 +1 @@ +build/html \ Kein Zeilenumbruch am Dateiende. diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/debian/spyder.links spyder-3.0.2+dfsg1/debian/spyder.links --- spyder-2.3.8+dfsg1/debian/spyder.links 2015-11-01 15:00:44.000000000 +0100 +++ spyder-3.0.2+dfsg1/debian/spyder.links 2016-10-30 19:22:44.000000000 +0100 @@ -1 +1 @@ -/usr/share/doc/python-spyderlib-doc/html /usr/share/doc/spyder/html +/usr/share/doc/spyder-doc/html /usr/share/doc/spyder/html diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/debian/spyder.menu spyder-3.0.2+dfsg1/debian/spyder.menu --- spyder-2.3.8+dfsg1/debian/spyder.menu 2015-11-01 15:00:44.000000000 +0100 +++ spyder-3.0.2+dfsg1/debian/spyder.menu 1970-01-01 01:00:00.000000000 +0100 @@ -1,2 +0,0 @@ -?package(spyder):needs="X11" section="Applications/Programming"\ - title="spyder" command="/usr/bin/spyder" diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/debian/watch spyder-3.0.2+dfsg1/debian/watch --- spyder-2.3.8+dfsg1/debian/watch 2015-11-01 15:00:44.000000000 +0100 +++ spyder-3.0.2+dfsg1/debian/watch 2016-10-30 19:22:44.000000000 +0100 @@ -1,5 +1,5 @@ version=3 opts=dversionmangle=s/\+(debian|dfsg|ds|deb)(\.\d+)?$//,\ repacksuffix=+dfsg1,\ -uversionmangle=s/(\d)[_\.\-\+]?((RC|rc|pre|dev|beta|alpha)\d*)$/$1~$2/,\ - https://github.com/spyder-ide/spyder/releases .*/spyder-(\d\S*)\.zip +uversionmangle=s/(rc|a|b|c)/~$1/ \ +https://pypi.debian.net/spyder/spyder-(.+)\.(?:zip|tgz|tbz|txz|(?:tar\.(?:gz|bz2|xz))) diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/doc/conf.py spyder-3.0.2+dfsg1/doc/conf.py --- spyder-2.3.8+dfsg1/doc/conf.py 2015-08-24 21:09:44.000000000 +0200 +++ spyder-3.0.2+dfsg1/doc/conf.py 2016-11-16 16:40:01.000000000 +0100 @@ -38,16 +38,16 @@ # General information about the project. project = 'Spyder' -copyright = '2009, Pierre Raybaut' +copyright = 'The Spyder Project Contributors' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -version = '2.3' +version = '3' # The full version, including alpha/beta/rc tags. -release = '2.3' +release = '3' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/doc/console.rst spyder-3.0.2+dfsg1/doc/console.rst --- spyder-2.3.8+dfsg1/doc/console.rst 2015-08-24 21:09:44.000000000 +0200 +++ spyder-3.0.2+dfsg1/doc/console.rst 2016-11-16 16:40:01.000000000 +0100 @@ -30,32 +30,7 @@ Related plugins: -* :doc:`inspector` +* :doc:`help` * :doc:`historylog` * :doc:`editor` -* :doc:`explorer` - - -Reloading modules: the User Module Deleter (UMD) ------------------------------------------------- - -When working with Python scripts interactively, one must keep in mind that -Python import a module from its source code (on disk) only when parsing the -first corresponding import statement. During this first import, the byte code -is generated (.pyc file) if necessary and the imported module code object is -cached in `sys.modules`. Then, when re-importing the same module, this cached -code object will be directly used even if the source code file (.py[w] file) -has changed meanwhile. - -This behavior is sometimes unexpected when working with the Python interpreter -in interactive mode, because one must either always restart the interpreter -or remove manually the .pyc files to be sure that changes made in imported -modules were taken into account. - -The User Module Deleter (UMD) is a Spyder console's exclusive feature that -forces the Python interpreter to reload modules completely when executing -a Python script. - -For example, when UMD is turned on, one may test complex applications -within the same Python interpreter without having to restart it every time -(restart time may be relatively long when testing GUI-based applications). \ Kein Zeilenumbruch am Dateiende. +* :doc:`fileexplorer` diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/doc/editor.rst spyder-3.0.2+dfsg1/doc/editor.rst --- spyder-2.3.8+dfsg1/doc/editor.rst 2015-08-24 21:09:44.000000000 +0200 +++ spyder-3.0.2+dfsg1/doc/editor.rst 2016-11-16 16:40:01.000000000 +0100 @@ -9,15 +9,27 @@ Function/class/method browser: +| + .. image:: images/editor1.png + :align: center +| Code analysis with `pyflakes`: +| + .. image:: images/editor2.png + :align: center +| Horizontal/vertical splitting feature: +| + .. image:: images/editor3.png + :align: center +| How to define a code cell -------------------------- @@ -37,5 +49,5 @@ Related plugins: * :doc:`console` -* :doc:`explorer` +* :doc:`fileexplorer` * :doc:`findinfiles` diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/doc/explorer.rst spyder-3.0.2+dfsg1/doc/explorer.rst --- spyder-2.3.8+dfsg1/doc/explorer.rst 2015-08-24 21:09:44.000000000 +0200 +++ spyder-3.0.2+dfsg1/doc/explorer.rst 1970-01-01 01:00:00.000000000 +0100 @@ -1,20 +0,0 @@ -Explorer -======== - -The explorer plugin is a file/directory browser allowing the user to open files -with the internal editor or with the appropriate application (Windows only). - -.. image:: images/explorer.png - -Context menus may be used to run a script, open a terminal window or run a -Windows explorer window (Windows only): - -.. image:: images/explorer_menu1.png - -.. image:: images/explorer_menu2.png - - -Related plugins: - -* :doc:`console` -* :doc:`editor` diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/doc/fileexplorer.rst spyder-3.0.2+dfsg1/doc/fileexplorer.rst --- spyder-2.3.8+dfsg1/doc/fileexplorer.rst 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/doc/fileexplorer.rst 2016-11-16 16:40:01.000000000 +0100 @@ -0,0 +1,33 @@ +File Explorer +============= + +The file explorer pane is a file/directory browser allowing the user to open +files with the internal editor or with the appropriate application (Windows +only). + +| + +.. image:: images/explorer.png + :align: center + +| + +Context menus may be used to run a script, open a terminal window or run a +Windows explorer window (Windows only): + +| + +.. image:: images/explorer_menu1.png + :align: center + +| + +.. image:: images/explorer_menu2.png + :align: center + +| + +Related plugins: + +* :doc:`ipythonconsole` +* :doc:`editor` diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/doc/help.rst spyder-3.0.2+dfsg1/doc/help.rst --- spyder-2.3.8+dfsg1/doc/help.rst 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/doc/help.rst 2016-10-25 02:05:22.000000000 +0200 @@ -0,0 +1,34 @@ +Help +==== + +The help plugin works together with the :doc:`console` and the +:doc:`editor`: it shows automatically documentation available when the +user is instantiating a class or calling a function (pressing the left +parenthesis key after a valid function or class name triggers a call +in the help pane). + +Note that this automatic link may be disabled by pressing the "Lock" button +(at the top right corner of the window). + +Of course, one can use the documentation viewer directly by entering an object +name in the editable combo box field, or by selecting old documentation requests +in the combo box. + +Plain text mode: + +.. image:: images/help_plain.png + +Rich text mode: + +.. image:: images/help_rich.png + +Sometimes, when docstrings are not available or not sufficient to document the +object, the documentation viewer can show the source code (if available, i.e. +if the object is pure Python): + +.. image:: images/help_source.png + +Related plugins: + +* :doc:`console` +* :doc:`editor` Binärdateien spyder-2.3.8+dfsg1/doc/images/arrayeditor.png und spyder-3.0.2+dfsg1/doc/images/arrayeditor.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/doc/images/console.png und spyder-3.0.2+dfsg1/doc/images/console.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/doc/images/dicteditor.png und spyder-3.0.2+dfsg1/doc/images/dicteditor.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/doc/images/editor1.png und spyder-3.0.2+dfsg1/doc/images/editor1.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/doc/images/editor2.png und spyder-3.0.2+dfsg1/doc/images/editor2.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/doc/images/editor3.png und spyder-3.0.2+dfsg1/doc/images/editor3.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/doc/images/explorer_menu1.png und spyder-3.0.2+dfsg1/doc/images/explorer_menu1.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/doc/images/explorer_menu2.png und spyder-3.0.2+dfsg1/doc/images/explorer_menu2.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/doc/images/explorer.png und spyder-3.0.2+dfsg1/doc/images/explorer.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/doc/images/findinfiles.png und spyder-3.0.2+dfsg1/doc/images/findinfiles.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/doc/images/git_install_dialog.png und spyder-3.0.2+dfsg1/doc/images/git_install_dialog.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/doc/images/help_plain.png und spyder-3.0.2+dfsg1/doc/images/help_plain.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/doc/images/help_rich.png und spyder-3.0.2+dfsg1/doc/images/help_rich.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/doc/images/help_source.png und spyder-3.0.2+dfsg1/doc/images/help_source.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/doc/images/historylog.png und spyder-3.0.2+dfsg1/doc/images/historylog.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/doc/images/inspector_plain.png und spyder-3.0.2+dfsg1/doc/images/inspector_plain.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/doc/images/inspector_rich.png und spyder-3.0.2+dfsg1/doc/images/inspector_rich.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/doc/images/inspector_source.png und spyder-3.0.2+dfsg1/doc/images/inspector_source.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/doc/images/internalconsole.png und spyder-3.0.2+dfsg1/doc/images/internalconsole.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/doc/images/ipythonconsolemenu.png und spyder-3.0.2+dfsg1/doc/images/ipythonconsolemenu.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/doc/images/ipythonconsole.png und spyder-3.0.2+dfsg1/doc/images/ipythonconsole.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/doc/images/ipythonkernelconnect.png und spyder-3.0.2+dfsg1/doc/images/ipythonkernelconnect.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/doc/images/lightmode.png und spyder-3.0.2+dfsg1/doc/images/lightmode.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/doc/images/listeditor.png und spyder-3.0.2+dfsg1/doc/images/listeditor.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/doc/images/new_project.png und spyder-3.0.2+dfsg1/doc/images/new_project.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/doc/images/onlinehelp.png und spyder-3.0.2+dfsg1/doc/images/onlinehelp.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/doc/images/projectexplorer2.png und spyder-3.0.2+dfsg1/doc/images/projectexplorer2.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/doc/images/projectexplorer.png und spyder-3.0.2+dfsg1/doc/images/projectexplorer.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/doc/images/pylint.png und spyder-3.0.2+dfsg1/doc/images/pylint.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/doc/images/texteditor.png und spyder-3.0.2+dfsg1/doc/images/texteditor.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/doc/images/variableexplorer1.png und spyder-3.0.2+dfsg1/doc/images/variableexplorer1.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/doc/images/variableexplorer2.png und spyder-3.0.2+dfsg1/doc/images/variableexplorer2.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/doc/images/variableexplorer-imshow.png und spyder-3.0.2+dfsg1/doc/images/variableexplorer-imshow.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/doc/images/variableexplorer-plot.png und spyder-3.0.2+dfsg1/doc/images/variableexplorer-plot.png sind verschieden. diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/doc/index.rst spyder-3.0.2+dfsg1/doc/index.rst --- spyder-2.3.8+dfsg1/doc/index.rst 2015-08-24 21:09:44.000000000 +0200 +++ spyder-3.0.2+dfsg1/doc/index.rst 2016-11-16 16:40:01.000000000 +0100 @@ -30,19 +30,18 @@ overview installation options - lightmode - console + editor ipythonconsole debugging + console variableexplorer - inspector - onlinehelp + help + projects + pylint + fileexplorer historylog - explorer - projectexplorer - editor findinfiles - pylint + onlinehelp internalconsole Indices and tables: diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/doc/inspector.rst spyder-3.0.2+dfsg1/doc/inspector.rst --- spyder-2.3.8+dfsg1/doc/inspector.rst 2015-08-24 21:09:44.000000000 +0200 +++ spyder-3.0.2+dfsg1/doc/inspector.rst 1970-01-01 01:00:00.000000000 +0100 @@ -1,34 +0,0 @@ -Object inspector -================ - -The object inspector plugin works together with the :doc:`console` and the -:doc:`editor`: it shows automatically documentation available when the -user is instantiating a class or calling a function (pressing the left -parenthesis key after a valid function or class name triggers the object -inspector). - -Note that this automatic link may be disabled by pressing the "Lock" button -(at the top right corner of the window). - -Of course, one can use the documentation viewer directly by entering an object -name in the editable combo box field, or by selecting old documentation requests -in the combo box. - -Plain text mode: - -.. image:: images/inspector_plain.png - -Rich text mode: - -.. image:: images/inspector_rich.png - -Sometimes, when docstrings are not available or not sufficient to document the -object, the documentation viewer can show the source code (if available, i.e. -if the object is pure Python): - -.. image:: images/inspector_source.png - -Related plugins: - -* :doc:`console` -* :doc:`editor` diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/doc/installation.rst spyder-3.0.2+dfsg1/doc/installation.rst --- spyder-2.3.8+dfsg1/doc/installation.rst 2015-10-12 02:00:38.000000000 +0200 +++ spyder-3.0.2+dfsg1/doc/installation.rst 2016-11-16 16:40:01.000000000 +0100 @@ -27,19 +27,13 @@ #. Install the essential requirements: - * `The Python language `_ - * `PyQt4 `_ + * `The Python programming language `_ + * `PyQt5 `_ (recommended) + or `PyQt4 `_ -#. Install optional modules: +#. Install Spyder and its dependencies by running this command:: - Please refer to the `Recommended modules`_ section to see what other packages - you might need. - -#. Installing Spyder itself: - - You need to download and install the .exe file that corresponds to your Python - version and architecture from - `this page `_. + pip install spyder Updating Spyder @@ -47,11 +41,16 @@ You can update Spyder by: -* Updating Anaconda, WinPython, Python(x,y). +* Updating Anaconda, WinPython or Python(x,y). + +* Or using this command (in case you *don't* use any of those scientific + distributions):: + + pip install --upgrade spyder + + .. note:: -* Installing a new .exe file from the page mentioned above (this will automatically - uninstall any previous version *only if* this version was installed with the same - kind of installer - i.e. not with an .msi installer). + This command will also update all Spyder dependencies | @@ -99,21 +98,20 @@ Installing on Linux ------------------- -Please refer to the `Recommended modules`_ section to see what other packages you +Please refer to the `Requirements`_ section to see what other packages you might need. #. **Ubuntu**: - + * Using the official package manager: ``sudo apt-get install spyder``. - + .. note:: - + This package could be slightly outdated. If you find that is the case, please use the Debian package mentioned below. - + * Using the `pip `_ package manager: - - * Requirements: ``sudo apt-get install python-qt4 python-sphinx`` + * Installing: ``sudo pip install spyder`` * Updating: ``sudo pip install -U spyder`` @@ -151,69 +149,81 @@ Requirements ~~~~~~~~~~~~ -The minimal requirements to run Spyder are - -* `Python `_ 2.6+ - -* `PyQt4 `_ >= v4.6 or - `PySide `_ >=1.2.0 (PyQt4 is recommended). - +The requirements to run Spyder are: -Recommended modules -~~~~~~~~~~~~~~~~~~~ +* `Python `_ 2.7 or >=3.3 -We recommend you to install these modules to get the most out of Spyder: +* `PyQt5 `_ >=5.2 or + `PyQt4 `_ >=4.6.0 + (PyQt5 is recommended). -* `IPython `_ 3.0 or less, or - `qtconsole `_ 4.0 or higher -- for an +* `Qtconsole `_ >=4.2.0 -- for an enhanced Python interpreter. - - .. note:: - - - On *Ubuntu* you need to install ``ipython-qtconsole``. - - On *Fedora*, ``ipython-gui`` - - And on *Gentoo* ``ipython`` with the ``qt4`` USE flag - -* `sphinx `_ >= v0.6 -- for the Object Inspector's rich - text mode and to get our documentation. -* `rope `_ 0.9.x (x>=0) -- for code completion, +* `Rope `_ >=0.9.4 and + `Jedi ` 0.8.1 -- for code completion, go-to-definition and calltips on the Editor. -* `pyflakes `_ 0.x (x>=5) -- for real-time +* `Pyflakes `_ -- for real-time code analysis. -* `pylint `_ -- for static code analysis. +* `Sphinx `_ -- for the Help pane rich text mode + and to get our documentation. -* `pep8 `_ -- for style analysis. +* `Pygments `_ >=2.0 -- for syntax highlighting and code + completion in the Editor of all file types it supports. -* `numpy `_ -- for N-dimensional arrays. +* `Pylint `_ -- for static code analysis. -* `scipy `_ -- for signal and image processing. +* `Pep8 `_ -- for style analysis. -* `matplotlib `_ -- for 2D and 3D plotting. - -* `psutil `_ -- for memory/CPU usage in the status +* `Psutil `_ -- for memory/CPU usage in the status bar. +* `Nbconvert `_ -- to manipulate Jupyter notebooks + on the Editor. + +* `Qtawesome `_ -- for an icon theme based on + FontAwesome. + +* Pickleshare -- To show import completions on the Editor and Consoles. + +* `PyZMQ `_ -- To run introspection services on the + Editor asynchronously. + +* `QtPy `_ >=1.1.0 -- To run Spyder with PyQt4 or + PyQt5 seamlessly. + + +Optional modules +~~~~~~~~~~~~~~~~ + +* `Matplotlib `_ >=1.0 -- for 2D and 3D plotting + in the consoles. + +* `Pandas `_ >=0.13.1 -- for view and editing DataFrames + and Series in the Variable Explorer. + +* `Numpy `_ -- for view and editing two or three + dimensional arrays in the Variable Explorer. + +* `Sympy `_ >=0.7.3 -- for working with symbolic mathematics + in the IPython console. + +* `Scipy `_ -- for importing Matlab workspace files in + the Variable Explorer. + Installation procedure ~~~~~~~~~~~~~~~~~~~~~~ -#. Download and unzip the source package (spyder-*version*.zip): -#. Change your current directory to the unzipped directory -#. Run: +1. If you use Anaconda, you need to run this command to install Spyder: - * ``sudo python setup.py install``, on Linux or MacOS X, or - * ``python setup.py install``, on Windows. + ``conda install spyder`` - .. warning:: +2. If you don't use Anaconda, you need to run: - This procedure does *not* uninstall previous versions of Spyder, it simply - copies files on top of an existing installation. When using this command, - it is thus highly recommended to uninstall manually any previous version of - Spyder by removing the associated directories (``spyderlib`` and - ``spyderplugins`` in your site-packages directory). + ``pip install --upgrade spyder`` Run without installing @@ -236,6 +246,8 @@ If you want to try the next Spyder version, you have to: +#. Install Spyder `requirements`_ + #. Install `Git `_, a powerful source control management tool. diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/doc/internalconsole.rst spyder-3.0.2+dfsg1/doc/internalconsole.rst --- spyder-2.3.8+dfsg1/doc/internalconsole.rst 2015-08-24 21:09:44.000000000 +0200 +++ spyder-3.0.2+dfsg1/doc/internalconsole.rst 2016-11-16 16:40:01.000000000 +0100 @@ -7,41 +7,13 @@ process as Spyder's, but the Internal Console may be executed in a separate thread (this is optional and for example this is not the case in Spyder itself). +| + .. image:: images/internalconsole.png + :align: center + +| The internal console support the following features: * Code completion and calltips -* User Module Deleter (as in :doc:`console`) - -Special commands ----------------- - -The following special commands are supported by the interactive console. - -- Edit script - - ``edit foobar.py`` will open ``foobar.py`` with Spyder's editor. - ``xedit foobar.py`` will open ``foobar.py`` with the external editor. - -- Execute script - - ``run foobar.py`` will execute ``foobar.py`` in interactive console. - -- Remove references - - ``clear x, y`` will remove references named ``x`` and ``y``. - -- Shell commands - - ``!cmd`` will execute system command ``cmd`` (example ``!ls`` on Linux or - ``!dir`` on Windows). - -- Python help - - ``object?`` will show ``object``'s help in documentation viewer. - -- GUI-based editor - - ``oedit(object)`` will open an appropriate GUI-based editor to modify object - ``object`` and will return the result. diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/doc/ipythonconsole.rst spyder-3.0.2+dfsg1/doc/ipythonconsole.rst --- spyder-2.3.8+dfsg1/doc/ipythonconsole.rst 2015-10-12 02:00:38.000000000 +0200 +++ spyder-3.0.2+dfsg1/doc/ipythonconsole.rst 2016-11-16 16:40:01.000000000 +0100 @@ -7,19 +7,32 @@ back end. Visit the IPython project website for full documentation of IPython's many features. +| + .. image:: images/ipythonconsole.png + :align: center +| From the Consoles menu, Spyder can launch **IPython Console** instances that attach to kernels that are managed by Spyder itself or it can connect to external kernels that are managed by IPython Qt Console sessions or the IPython Notebook. +| + .. image:: images/ipythonconsolemenu.png + :align: center + +| When "Connect to an existing kernel" is selected, Spyder prompts for the kernel connection file details: +| + .. image:: images/ipythonkernelconnect.png + :align: center +| **IPython Consoles** that are attached to kernels that were created by Spyder support the following features: @@ -40,9 +53,34 @@ debugging step commands to the kernel. Breakpoints must be set manually from the console command line. + +Reloading modules: the User Module Reloader (UMR) +------------------------------------------------- + +When working with Python scripts interactively, one must keep in mind that +Python import a module from its source code (on disk) only when parsing the +first corresponding import statement. During this first import, the byte code +is generated (.pyc file) if necessary and the imported module code object is +cached in `sys.modules`. Then, when re-importing the same module, this cached +code object will be directly used even if the source code file (.py[w] file) +has changed meanwhile. + +This behavior is sometimes unexpected when working with the Python interpreter +in interactive mode, because one must either always restart the interpreter +or remove manually the .pyc files to be sure that changes made in imported +modules were taken into account. + +The User Module Reloader (UMR) is a Spyder console's exclusive feature that +forces the Python interpreter to reload modules completely when executing +a Python script. + +For example, when UMR is turned on, one may test complex applications +within the same Python interpreter without having to restart it every time +(restart time may be relatively long when testing GUI-based applications). + + Related plugins: -* :doc:`inspector` +* :doc:`help` * :doc:`editor` -* :doc:`explorer` - +* :doc:`fileexplorer` diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/doc/lightmode.rst spyder-3.0.2+dfsg1/doc/lightmode.rst --- spyder-2.3.8+dfsg1/doc/lightmode.rst 2015-08-24 21:09:44.000000000 +0200 +++ spyder-3.0.2+dfsg1/doc/lightmode.rst 1970-01-01 01:00:00.000000000 +0100 @@ -1,22 +0,0 @@ -Light mode -========== - -Spyder may be started in *light mode* with the following command: - - ``python spyder.py --light`` - -or - - ``python spyder.py -l`` - - -The light mode is a very simple and light environment with the :doc:`console` -and the :doc:`variableexplorer`. - -.. image:: images/lightmode.png - -Related plugins: - -* :doc:`console` -* :doc:`variableexplorer` - diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/doc/onlinehelp.rst spyder-3.0.2+dfsg1/doc/onlinehelp.rst --- spyder-2.3.8+dfsg1/doc/onlinehelp.rst 2015-08-24 21:09:44.000000000 +0200 +++ spyder-3.0.2+dfsg1/doc/onlinehelp.rst 2016-10-25 02:05:22.000000000 +0200 @@ -9,4 +9,4 @@ Related plugins: -* :doc:`inspector` +* :doc:`help` diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/doc/options.rst spyder-3.0.2+dfsg1/doc/options.rst --- spyder-2.3.8+dfsg1/doc/options.rst 2015-11-27 14:29:54.000000000 +0100 +++ spyder-3.0.2+dfsg1/doc/options.rst 2016-10-25 02:05:22.000000000 +0200 @@ -2,21 +2,21 @@ ==================== Spyder's command line options are the following: -(type 'python spyder.py --help' to show the text below) Options: -h, --help show this help message and exit - -l, --light Light version (all add-ons are disabled) - --session=STARTUP_SESSION - Startup session - --defaults Reset to configuration settings to defaults + --new-instance Run a new instance of Spyder, even if the single + instance mode has been turned on (default) + --defaults Reset configuration settings to defaults --reset Remove all configuration files! --optimize Optimize Spyder bytecode (this may require administrative privileges) -w WORKING_DIRECTORY, --workdir=WORKING_DIRECTORY Default working directory - -d, --debug Debug mode (stds are not redirected) + --show-console Do not hide parent console window (Windows) --multithread Internal console is executed in another thread (separate from main application thread) --profile Profile mode (internal test, not related with Python - profiling) \ Kein Zeilenumbruch am Dateiende. + profiling) + --window-title=WINDOW_TITLE + String to show in the main window title diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/doc/overview.rst spyder-3.0.2+dfsg1/doc/overview.rst --- spyder-2.3.8+dfsg1/doc/overview.rst 2015-08-24 21:09:44.000000000 +0200 +++ spyder-3.0.2+dfsg1/doc/overview.rst 2016-11-16 16:40:01.000000000 +0100 @@ -16,7 +16,7 @@ * *preferences* dialog box: * keyboard shortcuts - * syntax coloring schemes (source editor, history log, object inspector) + * syntax coloring schemes (source editor, history log, help) * console: background color (black/white), automatic code completion, etc. * and a lot more... @@ -39,7 +39,7 @@ * *calltips* * *go-to-definition*: go to object (any symbol: function, class, attribute, etc.) definition by pressing Ctrl+Left mouse click on word or Ctrl+G (default shortcut) - * *occurence highlighting* + * *occurrence highlighting* * typing helpers (optional): * automatically insert closing parentheses, braces and brackets @@ -47,13 +47,13 @@ * *to-do* lists (TODO, FIXME, XXX) * errors/warnings (real-time *code analysis* provided by `pyflakes`) - * integrated *`pylint` code analysis* + * integrated static code analysis (using `pylint`) * direct link to `winpdb` external debugger * :doc:`console`: * *all consoles are executed in a separate process* - * *code completion*/calltips and automatic link to object inspector (see below) + * *code completion*/calltips and automatic link to help (see below) * open Python interpreters or basic terminal command windows * run Python scripts (see source editor features) * *variable explorer*: @@ -64,17 +64,17 @@ * data visualization * :doc:`historylog` -* :doc:`inspector`: +* :doc:`help`: * provide documentation or source code on any Python object (class, function, module, ...) * documentation may be displayed as an html page thanks to the rich text mode (powered by `sphinx`) * :doc:`onlinehelp`: automatically generated html documentation on installed Python modules -* :doc:`findinfiles`: find string occurences in a directory, a mercurial repository or directly in PYTHONPATH (support for regular expressions and included/excluded string lists) -* :doc:`explorer` -* :doc:`projectexplorer` (support Pydev project import) +* :doc:`findinfiles`: find string occurrences in a directory, a mercurial repository or directly in PYTHONPATH (support for regular expressions and included/excluded string lists) +* :doc:`fileexplorer` +* :doc:`projects` -Spyder may also be used as a PyQt4 or PySide extension library -(module 'spyderlib'). For example, the Python interactive shell widget -used in Spyder may be embedded in your own PyQt4 or PySide application. +Spyder may also be used as a PyQt5 or PyQt4 extension library +(module 'spyder'). For example, the Python interactive shell widget +used in Spyder may be embedded in your own PyQt5 or PyQt4 application. diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/doc/projectexplorer.rst spyder-3.0.2+dfsg1/doc/projectexplorer.rst --- spyder-2.3.8+dfsg1/doc/projectexplorer.rst 2015-08-24 21:09:44.000000000 +0200 +++ spyder-3.0.2+dfsg1/doc/projectexplorer.rst 1970-01-01 01:00:00.000000000 +0100 @@ -1,39 +0,0 @@ -Project Explorer -================ - -The project explorer plugin handles project management in Spyder with the -following main features: - -* import from existing Pydev (Eclipse) or Spyder projects -* add/remove project folders to/from Spyder's PYTHONPATH directly from - the context menu or manage these folders in a dedicated dialog box -* multiple file selection (for all available actions: open, rename, delete, - and so on) -* file type filters - -.. image:: images/projectexplorer.png - -.. image:: images/projectexplorer2.png - -Version Control Integration ---------------------------- - -Spyder has limited integration with Mercurial_ and Git_. Commit and browse -commands are available by right-clicking on relevant files that reside within -an already initialized repository. These menu picks -assume that certain commands are available on the system path. - -* For Mercurial repositories, TortoiseHG_ must be installed, and either ``thg`` - or ``hgtk`` must be on the system path. -* For git repositories, the commands ``git`` and ``gitk`` must be on the - system path. For Windows systems, the msysgit_ package provides a convenient - installer and the option to place common git commands on the system path without - creating conflicts with Windows system tools. - The second option in the dialog below is generally a safe approach. - -.. image:: images/git_install_dialog.png - -.. _Git: http://git-scm.com/ -.. _Mercurial: http://mercurial.selenic.com/ -.. _TortoiseHg: http://tortoisehg.bitbucket.org/ -.. _msysgit: https://code.google.com/p/msysgit/ diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/doc/projects.rst spyder-3.0.2+dfsg1/doc/projects.rst --- spyder-2.3.8+dfsg1/doc/projects.rst 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/doc/projects.rst 2016-11-16 16:40:01.000000000 +0100 @@ -0,0 +1,69 @@ +Projects +======== + +Spyder allows users to associate a given directory with a project. This has two +main advantages: + +1. Projects remember the list of open files in Editor. This permits to easily + work on several coding efforts at the same time. +2. The project's path is added to the list of paths Python looks modules for, so + that modules developed as part of a project can be easily imported in any + console. + +To create a project, it is necessary to select the *New Project* entry from the +*Projects* menu: + +| + +.. image:: images/new_project.png + :align: center + +| + +When a project is activated, the *Project explorer* pane is shown, which +presents a tree view structure of the current project + +| + +.. image:: images/projectexplorer.png + :align: center + +| + +Through this pane it is possible to make several operations on the files that +belong to project + +| + +.. image:: images/projectexplorer2.png + :align: center + +| + +.. note:: Projects are completely optional and not imposed on users, i.e. users + can work without creating any project. + + +Version Control Integration +--------------------------- + +Spyder has limited integration with Git_ and Mercurial_. Commit and browse +commands are available by right-clicking on relevant files that reside within +an already initialized repository. This menu assume that certain commands are +available on the system path. + +* For Mercurial repositories, TortoiseHG_ must be installed, and either ``thg`` + or ``hgtk`` must be on the system path. +* For git repositories, the commands ``git`` and ``gitk`` must be on the + system path. For Windows systems, the `Git for Windows`_ package provides a + convenient installer and the option to place common git commands on the + system path without creating conflicts with Windows system tools. + The second option in the dialog below is generally a safe approach. + +.. image:: images/git_install_dialog.png + :align: center + +.. _Git: http://git-scm.com/ +.. _Mercurial: http://mercurial.selenic.com/ +.. _TortoiseHg: http://tortoisehg.bitbucket.org/ +.. _Git for Windows: https://git-for-windows.github.io/ diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/doc/pylint.rst spyder-3.0.2+dfsg1/doc/pylint.rst --- spyder-2.3.8+dfsg1/doc/pylint.rst 2015-08-24 21:09:44.000000000 +0200 +++ spyder-3.0.2+dfsg1/doc/pylint.rst 2016-11-16 16:40:01.000000000 +0100 @@ -1,12 +1,17 @@ -Pylint extension -================ +Static code analysis +==================== -Pylint extension may be used directly from the :doc:`editor`, or by entering -manually the Python module or package path - i.e. it works either with `.py` -(or `.pyw`) Python scripts or with whole Python packages (directories containing -an `__init__.py` script). +The static code analysis tool may be used directly from the :doc:`editor`, or +by entering manually the Python module or package path - i.e. it works either +with `.py` (or `.pyw`) Python scripts or with whole Python packages +(directories containing an `__init__.py` script). + +| .. image:: images/pylint.png + :align: center + +| Related plugins: Binärdateien spyder-2.3.8+dfsg1/doc/spyder_bbg.png und spyder-3.0.2+dfsg1/doc/spyder_bbg.png sind verschieden. diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/doc/variableexplorer.rst spyder-3.0.2+dfsg1/doc/variableexplorer.rst --- spyder-2.3.8+dfsg1/doc/variableexplorer.rst 2015-08-24 21:09:44.000000000 +0200 +++ spyder-3.0.2+dfsg1/doc/variableexplorer.rst 2016-11-16 16:40:01.000000000 +0100 @@ -1,40 +1,51 @@ Variable Explorer ================= -The variable explorer shows the `globals()` namespace contents (i.e. all global -object references) of the current console: it supports both the :doc:`console` -(Python interpreter running in a remote process) -and the :doc:`internalconsole`. +The variable explorer shows the namespace contents (i.e. all global object +references) of the current console + +| .. image:: images/variableexplorer1.png + :align: center + +| The following screenshots show some interesting features such as editing lists, strings, dictionaries, NumPy arrays, or plotting/showing NumPy arrays data. +| + .. image:: images/listeditor.png + :align: center + +| .. image:: images/texteditor.png + :align: center + +| .. image:: images/dicteditor.png + :align: center + +| .. image:: images/arrayeditor.png + :align: center -.. image:: images/variableexplorer-plot.png +| -.. image:: images/variableexplorer-imshow.png +.. image:: images/variableexplorer-plot.png + :align: center -The default variable explorer configuration allows to browse global variables -without slowing the console even with very large NumPy arrays, lists or -dictionaries. The trick is to truncate values, to hide collection contents -(i.e. showing '' instead of list contents) and to *not* show -mininum and maximum values for NumPy arrays (see context menu options on the -screenshot at the top of this page). +| -However, most of the time, choosing the opposite options won't have too much -effect on console's performance: +.. image:: images/variableexplorer-imshow.png + :align: center -.. image:: images/variableexplorer2.png +| Supported types @@ -57,5 +68,4 @@ Related plugins: -* :doc:`console` -* :doc:`internalconsole` +* :doc:`ipythonconsole` Binärdateien spyder-2.3.8+dfsg1/img_src/spyder3.png und spyder-3.0.2+dfsg1/img_src/spyder3.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/img_src/spyder_light.ico und spyder-3.0.2+dfsg1/img_src/spyder_light.ico sind verschieden. Binärdateien spyder-2.3.8+dfsg1/img_src/spyder.png und spyder-3.0.2+dfsg1/img_src/spyder.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/img_src/spyder_reset.ico und spyder-3.0.2+dfsg1/img_src/spyder_reset.ico sind verschieden. diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/LICENSE spyder-3.0.2+dfsg1/LICENSE --- spyder-2.3.8+dfsg1/LICENSE 2015-11-27 14:29:54.000000000 +0100 +++ spyder-3.0.2+dfsg1/LICENSE 2016-10-25 02:05:22.000000000 +0200 @@ -1,7 +1,7 @@ Spyder License Agreement (MIT License) -------------------------------------- -Copyright (c) 2009-2013 Pierre Raybaut +Copyright (c) Spyder Project Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation @@ -31,35 +31,38 @@ License terms are included in any software used inside Spyder's source code. -Included images (spyderlib/images) +Included images (spyder/images) ---------------------------------- [1] Spyder License Agreement (see above) -spyderlib/images/arredit.png -spyderlib/images/dictedit.png -spyderlib/images/none.png -spyderlib/images/not_found.png -spyderlib/images/matplotlib.png -spyderlib/images/options.svg -spyderlib/images/set_workdir.png -spyderlib/images/splash.png -spyderlib/images/spyder.svg -spyderlib/images/spyder_light.svg -spyderlib/images/whole_words.png -spyderlib/images/win_env.png -spyderlib/images/actions/hist.png -spyderlib/images/actions/window_nofullscreen.png -spyderlib/images/editor/filelist.png -spyderlib/images/editor/function.png -spyderlib/images/editor/method.png -spyderlib/images/editor/private1.png -spyderlib/images/editor/private2.png -spyderlib/images/editor/cell.png -spyderlib/images/editor/blockcomment.png -spyderlib/images/editor/refactor.png -spyderlib/images/console/ipython_console.png -spyderlib/images/console/ipython_console_t.png +spyder/images/arredit.png +spyder/images/dictedit.png +spyder/images/none.png +spyder/images/not_found.png +spyder/images/matplotlib.png +spyder/images/options.svg +spyder/images/set_workdir.png +spyder/images/splash.png +spyder/images/spyder.svg +spyder/images/spyder_light.svg +spyder/images/whole_words.png +spyder/images/win_env.png +spyder/images/actions/hist.png +spyder/images/actions/window_nofullscreen.png +spyder/images/editor/attribute.png +spyder/images/editor/filelist.png +spyder/images/editor/function.png +spyder/images/editor/method.png +spyder/images/editor/module.png +spyder/images/editor/no_match.png +spyder/images/editor/private1.png +spyder/images/editor/private2.png +spyder/images/editor/cell.png +spyder/images/editor/blockcomment.png +spyder/images/editor/refactor.png +spyder/images/console/ipython_console.png +spyder/images/console/ipython_console_t.png [2] Crystal Project Icons @@ -68,276 +71,276 @@ Contact: everaldo@everaldo.com Copyright (c) 2006-2007 Everaldo Coelho. -spyderlib/images/advanced.png -spyderlib/images/arrow.png -spyderlib/images/bold.png -spyderlib/images/browser.png -spyderlib/images/ext_tools.png -spyderlib/images/font.png -spyderlib/images/genprefs.png -spyderlib/images/inspector.png -spyderlib/images/italic.png -spyderlib/images/pythonpath_mgr.png -spyderlib/images/upper_lower.png -spyderlib/images/vcs_browse.png -spyderlib/images/vcs_commit.png -spyderlib/images/actions/1downarrow.png -spyderlib/images/actions/1uparrow.png -spyderlib/images/actions/2downarrow.png -spyderlib/images/actions/2uparrow.png -spyderlib/images/actions/auto_reload.png -spyderlib/images/actions/browse_tab.png -spyderlib/images/actions/check.png -spyderlib/images/actions/cmdprompt.png -spyderlib/images/actions/configure.png -spyderlib/images/actions/copywop.png -spyderlib/images/actions/delete.png -spyderlib/images/actions/edit.png -spyderlib/images/actions/edit24.png -spyderlib/images/actions/editcopy.png -spyderlib/images/actions/editcut.png -spyderlib/images/actions/editdelete.png -spyderlib/images/actions/editpaste.png -spyderlib/images/actions/edit_add.png -spyderlib/images/actions/edit_remove.png -spyderlib/images/actions/eraser.png -spyderlib/images/actions/exit.png -spyderlib/images/actions/filter.png -spyderlib/images/actions/find.png -spyderlib/images/actions/findf.png -spyderlib/images/actions/findnext.png -spyderlib/images/actions/findprevious.png -spyderlib/images/actions/folder_new.png -spyderlib/images/actions/hide.png -spyderlib/images/actions/home.png -spyderlib/images/actions/imshow.png -spyderlib/images/actions/insert.png -spyderlib/images/actions/lock.png -spyderlib/images/actions/lock_open.png -spyderlib/images/actions/magnifier.png -spyderlib/images/actions/next.png -spyderlib/images/actions/options_less.png -spyderlib/images/actions/options_more.png -spyderlib/images/actions/plot.png -spyderlib/images/actions/previous.png -spyderlib/images/actions/redo.png -spyderlib/images/actions/reload.png -spyderlib/images/actions/rename.png -spyderlib/images/actions/replace.png -spyderlib/images/actions/show.png -spyderlib/images/actions/special_paste.png -spyderlib/images/actions/synchronize.png -spyderlib/images/actions/tooloptions.png -spyderlib/images/actions/undo.png -spyderlib/images/actions/up.png -spyderlib/images/actions/zoom_in.png -spyderlib/images/actions/zoom_out.png -spyderlib/images/console/clear.png -spyderlib/images/console/cmdprompt_t.png -spyderlib/images/console/console.png -spyderlib/images/console/environ.png -spyderlib/images/console/history.png -spyderlib/images/console/history24.png -spyderlib/images/console/kill.png -spyderlib/images/console/prompt.png -spyderlib/images/console/restart.png -spyderlib/images/console/syspath.png -spyderlib/images/editor/bug.png -spyderlib/images/editor/close_panel.png -spyderlib/images/editor/comment.png -spyderlib/images/editor/error.png -spyderlib/images/editor/file.png -spyderlib/images/editor/fromcursor.png -spyderlib/images/editor/gotoline.png -spyderlib/images/editor/highlight.png -spyderlib/images/editor/horsplit.png -spyderlib/images/editor/indent.png -spyderlib/images/editor/last_edit_location.png -spyderlib/images/editor/newwindow.png -spyderlib/images/editor/next_cursor.png -spyderlib/images/editor/next_wng.png -spyderlib/images/editor/outline_explorer.png -spyderlib/images/editor/outline_explorer_vis.png -spyderlib/images/editor/prev_cursor.png -spyderlib/images/editor/prev_wng.png -spyderlib/images/editor/select.png -spyderlib/images/editor/selectall.png -spyderlib/images/editor/todo_list.png -spyderlib/images/editor/uncomment.png -spyderlib/images/editor/unindent.png -spyderlib/images/editor/versplit.png -spyderlib/images/editor/wng_list.png -spyderlib/images/file/fileclose.png -spyderlib/images/file/filecloseall.png -spyderlib/images/file/fileimport.png -spyderlib/images/file/filenew.png -spyderlib/images/file/fileopen.png -spyderlib/images/file/filesave.png -spyderlib/images/file/filesaveas.png -spyderlib/images/file/print.png -spyderlib/images/file/save_all.png -spyderlib/images/filetypes/bat.png -spyderlib/images/filetypes/bmp.png -spyderlib/images/filetypes/c.png -spyderlib/images/filetypes/cc.png -spyderlib/images/filetypes/cfg.png -spyderlib/images/filetypes/chm.png -spyderlib/images/filetypes/cl.png -spyderlib/images/filetypes/cmd.png -spyderlib/images/filetypes/cpp.png -spyderlib/images/filetypes/css.png -spyderlib/images/filetypes/cxx.png -spyderlib/images/filetypes/diff.png -spyderlib/images/filetypes/doc.png -spyderlib/images/filetypes/exe.png -spyderlib/images/filetypes/f.png -spyderlib/images/filetypes/f77.png -spyderlib/images/filetypes/f90.png -spyderlib/images/filetypes/gif.png -spyderlib/images/filetypes/h.png -spyderlib/images/filetypes/hh.png -spyderlib/images/filetypes/hpp.png -spyderlib/images/filetypes/htm.png -spyderlib/images/filetypes/html.png -spyderlib/images/filetypes/hxx.png -spyderlib/images/filetypes/inf.png -spyderlib/images/filetypes/ini.png -spyderlib/images/filetypes/jpeg.png -spyderlib/images/filetypes/jpg.png -spyderlib/images/filetypes/js.png -spyderlib/images/filetypes/log.png -spyderlib/images/filetypes/nt.png -spyderlib/images/filetypes/patch.png -spyderlib/images/filetypes/pdf.png -spyderlib/images/filetypes/png.png -spyderlib/images/filetypes/pps.png -spyderlib/images/filetypes/properties.png -spyderlib/images/filetypes/ps.png -spyderlib/images/filetypes/pxd.png -spyderlib/images/filetypes/pxi.png -spyderlib/images/filetypes/pyx.png -spyderlib/images/filetypes/rar.png -spyderlib/images/filetypes/readme.png -spyderlib/images/filetypes/reg.png -spyderlib/images/filetypes/rej.png -spyderlib/images/filetypes/session.png -spyderlib/images/filetypes/tar.png -spyderlib/images/filetypes/tex.png -spyderlib/images/filetypes/tgz.png -spyderlib/images/filetypes/tif.png -spyderlib/images/filetypes/tiff.png -spyderlib/images/filetypes/txt.png -spyderlib/images/filetypes/xls.png -spyderlib/images/filetypes/xml.png -spyderlib/images/filetypes/zip.png -spyderlib/images/projects/add_to_path.png -spyderlib/images/projects/folder.png -spyderlib/images/projects/package.png -spyderlib/images/projects/pp_folder.png -spyderlib/images/projects/pp_package.png -spyderlib/images/projects/pp_project.png -spyderlib/images/projects/project.png -spyderlib/images/projects/project_closed.png -spyderlib/images/projects/pythonpath.png -spyderlib/images/projects/remove_from_path.png -spyderlib/images/projects/show_all.png +spyder/images/advanced.png +spyder/images/arrow.png +spyder/images/bold.png +spyder/images/browser.png +spyder/images/ext_tools.png +spyder/images/font.png +spyder/images/genprefs.png +spyder/images/help.png +spyder/images/italic.png +spyder/images/pythonpath_mgr.png +spyder/images/upper_lower.png +spyder/images/vcs_browse.png +spyder/images/vcs_commit.png +spyder/images/actions/1downarrow.png +spyder/images/actions/1uparrow.png +spyder/images/actions/2downarrow.png +spyder/images/actions/2uparrow.png +spyder/images/actions/auto_reload.png +spyder/images/actions/browse_tab.png +spyder/images/actions/check.png +spyder/images/actions/cmdprompt.png +spyder/images/actions/configure.png +spyder/images/actions/copywop.png +spyder/images/actions/delete.png +spyder/images/actions/edit.png +spyder/images/actions/edit24.png +spyder/images/actions/editcopy.png +spyder/images/actions/editcut.png +spyder/images/actions/editdelete.png +spyder/images/actions/editpaste.png +spyder/images/actions/edit_add.png +spyder/images/actions/edit_remove.png +spyder/images/actions/eraser.png +spyder/images/actions/exit.png +spyder/images/actions/filter.png +spyder/images/actions/find.png +spyder/images/actions/findf.png +spyder/images/actions/findnext.png +spyder/images/actions/findprevious.png +spyder/images/actions/folder_new.png +spyder/images/actions/hide.png +spyder/images/actions/home.png +spyder/images/actions/imshow.png +spyder/images/actions/insert.png +spyder/images/actions/lock.png +spyder/images/actions/lock_open.png +spyder/images/actions/magnifier.png +spyder/images/actions/next.png +spyder/images/actions/options_less.png +spyder/images/actions/options_more.png +spyder/images/actions/plot.png +spyder/images/actions/previous.png +spyder/images/actions/redo.png +spyder/images/actions/reload.png +spyder/images/actions/rename.png +spyder/images/actions/replace.png +spyder/images/actions/show.png +spyder/images/actions/special_paste.png +spyder/images/actions/synchronize.png +spyder/images/actions/tooloptions.png +spyder/images/actions/undo.png +spyder/images/actions/up.png +spyder/images/actions/zoom_in.png +spyder/images/actions/zoom_out.png +spyder/images/console/clear.png +spyder/images/console/cmdprompt_t.png +spyder/images/console/console.png +spyder/images/console/environ.png +spyder/images/console/history.png +spyder/images/console/history24.png +spyder/images/console/kill.png +spyder/images/console/prompt.png +spyder/images/console/restart.png +spyder/images/console/syspath.png +spyder/images/editor/bug.png +spyder/images/editor/close_panel.png +spyder/images/editor/comment.png +spyder/images/editor/error.png +spyder/images/editor/file.png +spyder/images/editor/fromcursor.png +spyder/images/editor/gotoline.png +spyder/images/editor/highlight.png +spyder/images/editor/horsplit.png +spyder/images/editor/indent.png +spyder/images/editor/last_edit_location.png +spyder/images/editor/newwindow.png +spyder/images/editor/next_cursor.png +spyder/images/editor/next_wng.png +spyder/images/editor/outline_explorer.png +spyder/images/editor/outline_explorer_vis.png +spyder/images/editor/prev_cursor.png +spyder/images/editor/prev_wng.png +spyder/images/editor/select.png +spyder/images/editor/selectall.png +spyder/images/editor/todo_list.png +spyder/images/editor/uncomment.png +spyder/images/editor/unindent.png +spyder/images/editor/versplit.png +spyder/images/editor/wng_list.png +spyder/images/file/fileclose.png +spyder/images/file/filecloseall.png +spyder/images/file/fileimport.png +spyder/images/file/filenew.png +spyder/images/file/fileopen.png +spyder/images/file/filesave.png +spyder/images/file/filesaveas.png +spyder/images/file/print.png +spyder/images/file/save_all.png +spyder/images/filetypes/bat.png +spyder/images/filetypes/bmp.png +spyder/images/filetypes/c.png +spyder/images/filetypes/cc.png +spyder/images/filetypes/cfg.png +spyder/images/filetypes/chm.png +spyder/images/filetypes/cl.png +spyder/images/filetypes/cmd.png +spyder/images/filetypes/cpp.png +spyder/images/filetypes/css.png +spyder/images/filetypes/cxx.png +spyder/images/filetypes/diff.png +spyder/images/filetypes/doc.png +spyder/images/filetypes/exe.png +spyder/images/filetypes/f.png +spyder/images/filetypes/f77.png +spyder/images/filetypes/f90.png +spyder/images/filetypes/gif.png +spyder/images/filetypes/h.png +spyder/images/filetypes/hh.png +spyder/images/filetypes/hpp.png +spyder/images/filetypes/htm.png +spyder/images/filetypes/html.png +spyder/images/filetypes/hxx.png +spyder/images/filetypes/inf.png +spyder/images/filetypes/ini.png +spyder/images/filetypes/jpeg.png +spyder/images/filetypes/jpg.png +spyder/images/filetypes/js.png +spyder/images/filetypes/log.png +spyder/images/filetypes/nt.png +spyder/images/filetypes/patch.png +spyder/images/filetypes/pdf.png +spyder/images/filetypes/png.png +spyder/images/filetypes/pps.png +spyder/images/filetypes/properties.png +spyder/images/filetypes/ps.png +spyder/images/filetypes/pxd.png +spyder/images/filetypes/pxi.png +spyder/images/filetypes/pyx.png +spyder/images/filetypes/rar.png +spyder/images/filetypes/readme.png +spyder/images/filetypes/reg.png +spyder/images/filetypes/rej.png +spyder/images/filetypes/session.png +spyder/images/filetypes/tar.png +spyder/images/filetypes/tex.png +spyder/images/filetypes/tgz.png +spyder/images/filetypes/tif.png +spyder/images/filetypes/tiff.png +spyder/images/filetypes/txt.png +spyder/images/filetypes/xls.png +spyder/images/filetypes/xml.png +spyder/images/filetypes/zip.png +spyder/images/projects/add_to_path.png +spyder/images/projects/folder.png +spyder/images/projects/package.png +spyder/images/projects/pp_folder.png +spyder/images/projects/pp_package.png +spyder/images/projects/pp_project.png +spyder/images/projects/project.png +spyder/images/projects/project_closed.png +spyder/images/projects/pythonpath.png +spyder/images/projects/remove_from_path.png +spyder/images/projects/show_all.png [3] GNU Lesser General Public License (Version 2.1) -spyderlib/images/qt.png -spyderlib/images/qtassistant.png -spyderlib/images/qtdesigner.png -spyderlib/images/qtlinguist.png -spyderlib/images/filetypes/ts.png -spyderlib/images/filetypes/ui.png +spyder/images/qt.png +spyder/images/qtassistant.png +spyder/images/qtdesigner.png +spyder/images/qtlinguist.png +spyder/images/filetypes/ts.png +spyder/images/filetypes/ui.png [4] GNU General Public License version 3 -spyderlib/images/pythonxy.png -spyderlib/images/vitables.png +spyder/images/pythonxy.png +spyder/images/vitables.png [5] MIT License -spyderlib/images/winpython.svg -spyderlib/images/chevron-left.png -spyderlib/images/chevron-right.png +spyder/images/winpython.svg +spyder/images/chevron-left.png +spyder/images/chevron-right.png [6] BSD License -spyderlib/images/scipy.png +spyder/images/scipy.png [7] Creative Commons Attribution 2.5 License (FamFamFam Silk icon set 1.3) -spyderlib/images/actions/collapse.png -spyderlib/images/actions/collapse_selection.png -spyderlib/images/actions/expand.png -spyderlib/images/actions/expand_selection.png -spyderlib/images/actions/restore.png -spyderlib/images/editor/class.png -spyderlib/images/editor/convention.png -spyderlib/images/editor/todo.png -spyderlib/images/editor/warning.png -spyderlib/images/filetypes/po.png -spyderlib/images/filetypes/pot.png +spyder/images/actions/collapse.png +spyder/images/actions/collapse_selection.png +spyder/images/actions/expand.png +spyder/images/actions/expand_selection.png +spyder/images/actions/restore.png +spyder/images/editor/class.png +spyder/images/editor/convention.png +spyder/images/editor/todo.png +spyder/images/editor/warning.png +spyder/images/filetypes/po.png +spyder/images/filetypes/pot.png [8] PSF LICENSE AGREEMENT FOR PYTHON -spyderlib/images/console/python.png -spyderlib/images/console/python_t.png -spyderlib/images/filetypes/py.png -spyderlib/images/filetypes/pyc.png -spyderlib/images/filetypes/pyw.png +spyder/images/console/python.png +spyder/images/console/python_t.png +spyder/images/filetypes/py.png +spyder/images/filetypes/pyc.png +spyder/images/filetypes/pyw.png [9] zlib/libpng license -spyderlib/images/filetypes/nsh.png -spyderlib/images/filetypes/nsi.png +spyder/images/filetypes/nsh.png +spyder/images/filetypes/nsi.png [10] Eclipse Public License -spyderlib/images/projects/pydev.png +spyder/images/projects/pydev.png [11] Creative Commons Attribution 3.0 License (Yusuke Kamiyamane Icons) -spyderlib/images/actions/arrow-continue.png -spyderlib/images/actions/arrow-step-in.png -spyderlib/images/actions/arrow-step-out.png -spyderlib/images/actions/arrow-step-over.png -spyderlib/images/actions/maximize.png -spyderlib/images/actions/stop_debug.png -spyderlib/images/actions/unmaximize.png -spyderlib/images/actions/window_fullscreen.png -spyderlib/images/editor/breakpoint_big.png -spyderlib/images/editor/breakpoint_cond_big.png -spyderlib/images/editor/breakpoint_cond_small.png -spyderlib/images/editor/breakpoint_small.png -spyderlib/images/editor/debug.png -spyderlib/images/console/terminated.png -spyderlib/images/actions/stop.png +spyder/images/actions/arrow-continue.png +spyder/images/actions/arrow-step-in.png +spyder/images/actions/arrow-step-out.png +spyder/images/actions/arrow-step-over.png +spyder/images/actions/maximize.png +spyder/images/actions/stop_debug.png +spyder/images/actions/unmaximize.png +spyder/images/actions/window_fullscreen.png +spyder/images/editor/breakpoint_big.png +spyder/images/editor/breakpoint_cond_big.png +spyder/images/editor/breakpoint_cond_small.png +spyder/images/editor/breakpoint_small.png +spyder/images/editor/debug.png +spyder/images/console/terminated.png +spyder/images/actions/stop.png [12] Creative Commons BY-SA license (The Oxygen icon theme) -spyderlib/images/editor/run.png -spyderlib/images/editor/run_again.png -spyderlib/images/editor/run_cell.png -spyderlib/images/editor/run_cell_advance.png -spyderlib/images/editor/run_selection.png -spyderlib/images/editor/run_settings.png -spyderlib/images/console/run_small.png +spyder/images/editor/run.png +spyder/images/editor/run_again.png +spyder/images/editor/run_cell.png +spyder/images/editor/run_cell_advance.png +spyder/images/editor/run_selection.png +spyder/images/editor/run_settings.png +spyder/images/console/run_small.png [13] http://preloaders.net/ (According to the website: "All animated GIF and APNG images are completely free to use in all projects (web and desktop applications, freeware and commercial projects)") -spyderlib/images/console/loading_sprites.png +spyder/images/console/loading_sprites.png diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/MANIFEST.in spyder-3.0.2+dfsg1/MANIFEST.in --- spyder-2.3.8+dfsg1/MANIFEST.in 2015-11-27 14:29:54.000000000 +0100 +++ spyder-3.0.2+dfsg1/MANIFEST.in 2016-10-25 02:05:22.000000000 +0200 @@ -1,7 +1,10 @@ -recursive-include spyderlib *.pot *.po *.svg *.png *.css *.qss -recursive-include spyderlibplugins *.pot *.po *.svg *.png +recursive-include spyder *.pot *.po *.svg *.png *.css *.qss +recursive-include spyder_breakpoints *.pot *.po *.svg *.png +recursive-include spyder_profiler *.pot *.po *.svg *.png +recursive-include spyder_pylint *.pot *.po *.svg *.png recursive-include doc *.py *.rst *.png *.ico *. -recursive-include app_example *.py *.pyw *.bat *.qm *.svg *.png +include spyder/fonts/spyder.ttf +include spyder/fonts/spyder-charmap.json include scripts/* include img_src/*.ico include img_src/spyder.png diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/.pc/0001-fix-spyderlib-path.patch/spyder/__init__.py spyder-3.0.2+dfsg1/.pc/0001-fix-spyderlib-path.patch/spyder/__init__.py --- spyder-2.3.8+dfsg1/.pc/0001-fix-spyderlib-path.patch/spyder/__init__.py 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/.pc/0001-fix-spyderlib-path.patch/spyder/__init__.py 2016-11-20 23:51:59.000000000 +0100 @@ -0,0 +1,89 @@ +# -*- coding: utf-8 -*- +""" +Spyder License Agreement (MIT License) +-------------------------------------- + +Copyright (c) Spyder Project Contributors + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. +""" + +version_info = (3, 0, 2) + +__version__ = '.'.join(map(str, version_info)) +__license__ = __doc__ +__project_url__ = 'https://github.com/spyder-ide/spyder' +__forum_url__ = 'http://groups.google.com/group/spyderlib' + +# Dear (Debian, RPM, ...) package makers, please feel free to customize the +# following path to module's data (images) and translations: +DATAPATH = LOCALEPATH = DOCPATH = MATHJAXPATH = JQUERYPATH = '' + + +import os +# Directory of the current file +__dir__ = os.path.dirname(os.path.abspath(__file__)) + + +def add_to_distribution(dist): + """Add package to py2exe/cx_Freeze distribution object + Extension to guidata.disthelpers""" + try: + dist.add_qt_bindings() + except AttributeError: + raise ImportError("This script requires guidata 1.5+") + for _modname in ('spyder', 'spyderplugins'): + dist.add_module_data_files(_modname, ("", ), + ('.png', '.svg', '.html', '.png', '.txt', + '.js', '.inv', '.ico', '.css', '.doctree', + '.qm', '.py',), + copy_to_root=False) + + +def get_versions(reporev=True): + """Get version information for components used by Spyder""" + import sys + import platform + + import qtpy + import qtpy.QtCore + + revision = None + if reporev: + from spyder.utils import vcs + revision, branch = vcs.get_git_revision(os.path.dirname(__dir__)) + + if not sys.platform == 'darwin': # To avoid a crash with our Mac app + system = platform.system() + else: + system = 'Darwin' + + return { + 'spyder': __version__, + 'python': platform.python_version(), # "2.7.3" + 'bitness': 64 if sys.maxsize > 2**32 else 32, + 'qt': qtpy.QtCore.__version__, + 'qt_api': qtpy.API_NAME, # PyQt5 or PyQt4 + 'qt_api_ver': qtpy.PYQT_VERSION, + 'system': system, # Linux, Windows, ... + 'revision': revision, # '9fdf926eccce' + } diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/.pc/0001-fix-spyderlib-path.patch/spyderlib/__init__.py spyder-3.0.2+dfsg1/.pc/0001-fix-spyderlib-path.patch/spyderlib/__init__.py --- spyder-2.3.8+dfsg1/.pc/0001-fix-spyderlib-path.patch/spyderlib/__init__.py 2015-11-27 14:34:38.000000000 +0100 +++ spyder-3.0.2+dfsg1/.pc/0001-fix-spyderlib-path.patch/spyderlib/__init__.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,87 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Spyder License Agreement (MIT License) --------------------------------------- - -Copyright (c) 2009-2013 Pierre Raybaut -Copyright (c) 2013-2015 The Spyder Development Team - -Permission is hereby granted, free of charge, to any person -obtaining a copy of this software and associated documentation -files (the "Software"), to deal in the Software without -restriction, including without limitation the rights to use, -copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -OTHER DEALINGS IN THE SOFTWARE. -""" - -__version__ = '2.3.8' -__license__ = __doc__ -__project_url__ = 'https://github.com/spyder-ide/spyder' -__forum_url__ = 'http://groups.google.com/group/spyderlib' - -# Dear (Debian, RPM, ...) package makers, please feel free to customize the -# following path to module's data (images) and translations: -DATAPATH = LOCALEPATH = DOCPATH = MATHJAXPATH = JQUERYPATH = '' - - -import os -# Directory of the current file -__dir__ = os.path.dirname(os.path.abspath(__file__)) - - -def add_to_distribution(dist): - """Add package to py2exe/cx_Freeze distribution object - Extension to guidata.disthelpers""" - try: - dist.add_qt_bindings() - except AttributeError: - raise ImportError("This script requires guidata 1.5+") - for _modname in ('spyderlib', 'spyderplugins'): - dist.add_module_data_files(_modname, ("", ), - ('.png', '.svg', '.html', '.png', '.txt', - '.js', '.inv', '.ico', '.css', '.doctree', - '.qm', '.py',), - copy_to_root=False) - - -def get_versions(reporev=True): - """Get version information for components used by Spyder""" - import sys - import platform - import spyderlib.qt - import spyderlib.qt.QtCore - - revision = None - if reporev: - from spyderlib.utils import vcs - revision = vcs.get_git_revision(os.path.dirname(__dir__)) - - if not sys.platform == 'darwin': # To avoid a crash with our Mac app - system = platform.system() - else: - system = 'Darwin' - - return { - 'spyder': __version__, - 'python': platform.python_version(), # "2.7.3" - 'bitness': 64 if sys.maxsize > 2**32 else 32, - 'qt': spyderlib.qt.QtCore.__version__, - 'qt_api': spyderlib.qt.API_NAME, # PySide or PyQt4 - 'qt_api_ver': spyderlib.qt.__version__, - 'system': system, # Linux, Windows, ... - 'revision': revision, # '9fdf926eccce' - } diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/PKG-INFO spyder-3.0.2+dfsg1/PKG-INFO --- spyder-2.3.8+dfsg1/PKG-INFO 2015-11-27 14:35:54.000000000 +0100 +++ spyder-3.0.2+dfsg1/PKG-INFO 2016-11-20 23:54:14.000000000 +0100 @@ -1,31 +1,26 @@ Metadata-Version: 1.1 Name: spyder -Version: 2.3.8 +Version: 3.0.2 Summary: Scientific PYthon Development EnviRonment Home-page: https://github.com/spyder-ide/spyder -Author: Pierre Raybaut +Author: The Spyder Project Contributors Author-email: UNKNOWN License: MIT -Download-URL: https://github.com/spyder-ide/spyder/files/spyder-2.3.8.zip -Description: Spyder is an interactive Python development environment providing +Download-URL: https://github.com/spyder-ide/spyder/files/spyder-3.0.2.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 PyQt4 or - PySide application: source code editor with syntax highlighting and - code introspection/analysis features, NumPy array editor, dictionary + It also provides ready-to-use pure-Python widgets to your PyQt5 or + PyQt4 application: source code editor with syntax highlighting and + code introspection/analysis features, NumPy array editor, dictionary editor, Python console, etc. -Keywords: PyQt4 PySide editor shell console widgets IDE +Keywords: PyQt5 PyQt4 editor shell console widgets IDE Platform: any Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: MacOS Classifier: Operating System :: Microsoft :: Windows -Classifier: Operating System :: OS Independent -Classifier: Operating System :: POSIX -Classifier: Operating System :: Unix +Classifier: Operating System :: POSIX :: Linux Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Development Status :: 5 - Production/Stable Classifier: Topic :: Scientific/Engineering Classifier: Topic :: Software Development :: Widget Sets -Requires: rope (>=0.9.2) -Requires: sphinx (>=0.6.0) -Requires: PyQt4 (>=4.4) diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/README.md spyder-3.0.2+dfsg1/README.md --- spyder-2.3.8+dfsg1/README.md 2015-11-27 14:29:54.000000000 +0100 +++ spyder-3.0.2+dfsg1/README.md 2016-11-16 16:40:01.000000000 +0100 @@ -1,9 +1,16 @@ # Spyder - The Scientific PYthon Development EnviRonment -Copyright © 2009-2013 Pierre Raybaut. -Licensed under the terms of the MIT License -(see `spyderlib/__init__.py` for details) +Copyright © Spyder Project Contributors. +## Project details +[![license](https://img.shields.io/pypi/l/spyder.svg)](./LICENSE) +[![pypi version](https://img.shields.io/pypi/v/spyder.svg)](https://pypi.python.org/pypi/spyder) +[![Join the chat at https://gitter.im/spyder-ide/public](https://badges.gitter.im/spyder-ide/spyder.svg)](https://gitter.im/spyder-ide/public) + +## Build status +[![Travis status](https://travis-ci.org/spyder-ide/spyder.svg?branch=master)](https://travis-ci.org/spyder-ide/spyder) +[![AppVeyor status](https://ci.appveyor.com/api/projects/status/awb92if4tl555fuy/branch/master?svg=true)](https://ci.appveyor.com/project/ccordoba12/spyder/branch/master) +[![Coverage Status](https://coveralls.io/repos/github/spyder-ide/spyder/badge.svg?branch=master)](https://coveralls.io/github/spyder-ide/spyder?branch=master) ## Overview @@ -11,40 +18,40 @@ Spyder is a Python development environment with a lot of features: -* Editor +* **Editor** Multi-language editor with function/class browser, code analysis features (pyflakes and pylint are currently supported), code completion, horizontal and vertical splitting, and goto definition. -* Interactive console +* **Interactive console** Python or IPython consoles with workspace and debugging support to instantly evaluate the code written in the Editor. It also comes with Matplotlib figures integration. -* Documentation viewer +* **Documentation viewer** Show documentation for any class or function call made either in the Editor or a Console. -* Variable explorer +* **Variable explorer** Explore variables created during the execution of a file. Editing them is also possible with several GUI based editors, like a dictionary and Numpy array ones. -* Find in files feature +* **Find in files** Supporting regular expressions and mercurial repositories -* File/directories explorer +* **File explorer** -* History log +* **History log** -Spyder may also be used as a PyQt4/PySide extension library (module -`spyderlib`). For example, the Python interactive shell widget used in -Spyder may be embedded in your own PyQt4/PySide application. +Spyder may also be used as a PyQt5/PyQt4 extension library (module +`spyder`). For example, the Python interactive shell widget used in +Spyder may be embedded in your own PyQt5/PyQt4 application. ## Documentation @@ -61,122 +68,112 @@ the `bootstrap` script (see next section). The easiest way to install Spyder is: - -* On Windows: - - Using one of our executable installers, which can be found - [here](https://github.com/spyder-ide/spyder/releases). - - Or using one of these scientific Python distributions: - 1. [Python(x,y)](http://pythonxy.googlecode.com) - 2. [WinPython](https://winpython.github.io/) - 3. [Anaconda](http://continuum.io/downloads) - -* On Mac OSX: - - - Using our DMG installer, which can be found - [here](https://github.com/spyder-ide/spyder/releases). - - Using the [Anaconda Distribution](http://continuum.io/downloads). - - Through [MacPorts](http://www.macports.org/). - -* On GNU/Linux - - - Through your distribution package manager (i.e. `apt-get`, `yum`, - etc). - - Using the [Anaconda Distribution](http://continuum.io/downloads). - - Installing from source (see below). - -### Installing from source - -You can also install Spyder from its zip source package. For that you need to -download and uncompress the file called `spyder-x.y.z.zip`, which can be -found [here](https://github.com/spyder-ide/spyder/releases). Then you need to -use the integrated `setup.py` script that comes with it and which is based -on the Python standard library `distutils` module, with the following command: - - python setup.py install - -Note that `distutils` does *not* uninstall previous versions of Python -packages: it simply copies files on top of an existing installation. -When using this command, it is thus highly recommended to uninstall -manually any previous version of Spyder by removing the associated -directories ('spyderlib' and 'spyderplugins') from your site-packages -directory). - -From the [Python package index](http://pypi.python.org/pypi), you also -may install Spyder *and* upgrade an existing installation using `pip` -with this command +### On Windows: - pip install --upgrade spyder +Using one (and only one) of these scientific Python distributions: -For more details on supported platforms, please go to -. +1. [Anaconda](http://continuum.io/downloads) +2. [WinPython](https://winpython.github.io/) +3. [Python(x,y)](http://python-xy.github.io) +### On Mac OSX: -## Dependencies +- Using our DMG installer, which can be found + [here](https://github.com/spyder-ide/spyder/releases). +- Using the [Anaconda Distribution](http://continuum.io/downloads). +- Through [MacPorts](http://www.macports.org/). -*Imnportant note*: Most if not all the dependencies listed below come -with Python(x,y), WinPython and Anaconda, so you don't need to install -them separately when installing one of these scientific Python -distributions. +### On GNU/Linux -### Build dependencies +- Through your distribution package manager (i.e. `apt-get`, `yum`, + etc). +- Using the [Anaconda Distribution](http://continuum.io/downloads). +- Installing from source (see below). -When installing Spyder from its source package (using the command -`python setup.py install`), the only requirements is to have a Python version -greater than 2.6. +### Cross-platform way from source -### Runtime dependencies +You can also install Spyder with the `pip` package manager, which comes by +default with most Python installations. For that you need to use the +command: -* Python 2.6+ + pip install spyder -* PyQt4 4.6+ or PySide 1.2.0+ (PyQt4 is recommended) - -### Recommended modules +To upgrade Spyder to its latest version, if it was installed before, you need +to run -* Rope v0.9.2+ (editor code completion, calltips and go-to-definition) + pip install --upgrade spyder -* Pyflakes v0.5.0+ (real-time code analysis) +For more details on supported platforms, please refer to our +[installation instructions](http://pythonhosted.org/spyder/installation.html). -* Sphinx v0.6+ (object inspector's rich text mode) +**Important note**: This does not install the graphical Python libraries (i.e. +PyQt5 or PyQt4) that Spyder depends on. Those have to be installed separately +after installing Python. -* Numpy (N-dimensional arrays) -* Scipy (signal/image processing) +## Running from source -* Matplotlib (2D/3D plotting) +The fastest way to run Spyder is to get the source code using git, install +PyQt5 or PyQt4, and run these commands: -* IPython 3.0- or qtconsole 4.0+ (enhanced Python interpreter) +1. Install our *runtime dependencies* (see below). +2. `cd /your/spyder/git-clone` +3. `python bootstrap.py` - In Ubuntu you need to install `ipython-qtconsole`, on Fedora - `ipython-gui` and on Gentoo `ipython` with the `qt4` USE flag. +You may want to do this for fixing bugs in Spyder, adding new +features, learning how Spyder works or just getting a taste of it. -### Optional modules -* Pygments (syntax highlighting for several file types). +## Dependencies -* Pylint (static code analysis). +**Important note**: Most if not all the dependencies listed below come +with *Anaconda*, *WinPython* and *Python(x,y)*, so you don't need to install +them separately when installing one of these Scientific Python +distributions. -* Pep8 (style analysis). +### Build dependencies -## Running from source +When installing Spyder from its source package, the only requirement is to have +a Python version greater than 2.7 (Python 3.2 is not supported anymore). -It is possible to run Spyder directly (i.e. without installation) -from the unpacked zip folder (see *Installing from source*) using -Spyder's bootstrap script like this: +### Runtime dependencies - python bootstrap.py +* **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 + and go-to-definition. +* **Pyflakes**: Real-time code analysis. +* **Sphinx**: Rich text mode for the Help pane. +* **Pygments** 2.0+: Syntax highlighting for all file types it supports. +* **Pylint**: Static code analysis. +* **Pep8**: Style analysis. +* **Psutil**: CPU and memory usage on the status bar. +* **Nbconvert**: Manipulation of notebooks in the Editor. +* **Qtawesome**: To have an icon theme based on FontAwesome. +* **Pickleshare**: Show import completions on the Python consoles. +* **PyZMQ**: Run introspection services asynchronously. +* **QtPy** 1.1.0+: Abstracion layer for Python Qt bindings so that Spyder can run on PyQt4 + and PyQt5. + +### Optional dependencies + +* **Matplotlib**: 2D/3D plotting in the Python and IPython consoles. +* **Pandas**: View and edit DataFrames and Series in the Variable Explorer. +* **Numpy**: View and edit two or three dimensional arrays in the Variable Explorer. +* **SymPy**: Symbolic mathematics in the IPython console. +* **SciPy**: Import Matlab workspace files in the Variable Explorer. -This is especially useful for beta-testing, troubleshooting -and development of Spyder itself. ## More information -* For code development please go to +* For code development please go to: - -* For bug reports and feature requests - + +* For bug reports and feature requests: + * For discussions and troubleshooting: diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/scripts/spyder spyder-3.0.2+dfsg1/scripts/spyder --- spyder-2.3.8+dfsg1/scripts/spyder 2015-08-24 21:09:44.000000000 +0200 +++ spyder-3.0.2+dfsg1/scripts/spyder 2016-10-25 02:05:22.000000000 +0200 @@ -1,3 +1,3 @@ #!/usr/bin/env python -from spyderlib import start_app -start_app.main() \ Kein Zeilenumbruch am Dateiende. +from spyder.app import start +start.main() diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/scripts/spyder3 spyder-3.0.2+dfsg1/scripts/spyder3 --- spyder-2.3.8+dfsg1/scripts/spyder3 2015-08-24 21:09:44.000000000 +0200 +++ spyder-3.0.2+dfsg1/scripts/spyder3 2016-10-25 02:05:22.000000000 +0200 @@ -1,3 +1,3 @@ #! /usr/bin/python3 -from spyderlib import start_app -start_app.main() \ Kein Zeilenumbruch am Dateiende. +from spyder.app import start +start.main() diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/scripts/spyder_win_post_install.py spyder-3.0.2+dfsg1/scripts/spyder_win_post_install.py --- spyder-2.3.8+dfsg1/scripts/spyder_win_post_install.py 2015-11-27 14:29:54.000000000 +0100 +++ spyder-3.0.2+dfsg1/scripts/spyder_win_post_install.py 2016-10-25 02:05:22.000000000 +0200 @@ -95,7 +95,7 @@ file=sys.stderr) sys.exit(1) from win32com.shell import shell, shellcon - + path_names = ['CSIDL_COMMON_STARTMENU', 'CSIDL_STARTMENU', 'CSIDL_COMMON_APPDATA', 'CSIDL_LOCAL_APPDATA', 'CSIDL_APPDATA', 'CSIDL_COMMON_DESKTOPDIRECTORY', @@ -131,10 +131,12 @@ python = osp.abspath(osp.join(sys.prefix, 'python.exe')) pythonw = osp.abspath(osp.join(sys.prefix, 'pythonw.exe')) script = osp.abspath(osp.join(sys.prefix, 'scripts', 'spyder')) + if not osp.exists(script): # if not installed to the site scripts dir + script = osp.abspath(osp.join(osp.dirname(osp.abspath(__file__)), 'spyder')) workdir = "%HOMEDRIVE%%HOMEPATH%" import distutils.sysconfig lib_dir = distutils.sysconfig.get_python_lib(plat_specific=1) - ico_dir = osp.join(lib_dir, 'spyderlib', 'windows') + ico_dir = osp.join(lib_dir, 'spyder', 'windows') # if user is running -install manually then icons are in Scripts/ if not osp.isdir(ico_dir): ico_dir = osp.dirname(osp.abspath(__file__)) @@ -145,13 +147,6 @@ osp.join(ico_dir, 'spyder.ico')) file_created(fname) - desc += '. Light configuration: console and variable explorer only.' - fname = osp.join(start_menu, 'Spyder (light).lnk') - create_shortcut(python, desc, fname, - '"%s" --light' % script, workdir, - osp.join(ico_dir, 'spyder_light.ico')) - file_created(fname) - fname = osp.join(start_menu, 'Spyder-Reset all settings.lnk') create_shortcut(python, 'Reset Spyder settings to defaults', fname, '"%s" --reset' % script, workdir) diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/setup.cfg spyder-3.0.2+dfsg1/setup.cfg --- spyder-2.3.8+dfsg1/setup.cfg 2015-11-27 14:35:54.000000000 +0100 +++ spyder-3.0.2+dfsg1/setup.cfg 1970-01-01 01:00:00.000000000 +0100 @@ -1,5 +0,0 @@ -[egg_info] -tag_date = 0 -tag_svn_revision = 0 -tag_build = - diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/setup.py spyder-3.0.2+dfsg1/setup.py --- spyder-2.3.8+dfsg1/setup.py 2015-11-27 14:29:54.000000000 +0100 +++ spyder-3.0.2+dfsg1/setup.py 2016-10-25 02:05:22.000000000 +0200 @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- # -# Copyright © 2009-2010 Pierre Raybaut +# Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) +# (see spyder/__init__.py for details) """ Spyder @@ -17,31 +17,42 @@ import os.path as osp import subprocess import sys -import re import shutil from distutils.core import setup from distutils.command.build import build +from distutils.command.install import install from distutils.command.install_data import install_data + +#============================================================================== # Check for Python 3 +#============================================================================== PY3 = sys.version_info[0] == 3 -# This is necessary to prevent an error while installing Spyder with pip -# See http://stackoverflow.com/a/18961843/438386 -with_setuptools = False -if 'USE_SETUPTOOLS' in os.environ or 'pip' in __file__ or \ - 'VIRTUAL_ENV' in os.environ: - try: - from setuptools.command.install import install - with_setuptools = True - except: - with_setuptools = False -if not with_setuptools: - from distutils.command.install import install # analysis:ignore +#============================================================================== +# Minimal Python version sanity check +# Taken from the notebook setup.py -- Modified BSD License +#============================================================================== +v = sys.version_info +if v[:2] < (2,7) or (v[0] >= 3 and v[:2] < (3,3)): + error = "ERROR: Spyder requires Python version 2.7 or 3.3 or above." + print(error, file=sys.stderr) + sys.exit(1) + + +#============================================================================== +# Constants +#============================================================================== +NAME = 'spyder' +LIBNAME = 'spyder' +from spyder import __version__, __project_url__ +#============================================================================== +# Auxiliary functions +#============================================================================== def get_package_data(name, extlist): """Return data files for package *name* with extensions in *extlist*""" flist = [] @@ -74,12 +85,28 @@ ('share/pixmaps', ['img_src/spyder.png'])] elif os.name == 'nt': data_files = [('scripts', ['img_src/spyder.ico', - 'img_src/spyder_light.ico'])] + 'img_src/spyder_reset.ico'])] else: data_files = [] return data_files +def get_packages(): + """Return package list""" + packages = ( + get_subpackages(LIBNAME) + + get_subpackages('spyder_breakpoints') + + get_subpackages('spyder_profiler') + + get_subpackages('spyder_pylint') + + get_subpackages('spyder_io_dcm') + + get_subpackages('spyder_io_hdf5') + ) + return packages + + +#============================================================================== +# Make Linux detect Spyder desktop file +#============================================================================== class MyInstallData(install_data): def run(self): install_data.run(self) @@ -92,7 +119,9 @@ CMDCLASS = {'install_data': MyInstallData} +#============================================================================== # Sphinx build (documentation) +#============================================================================== def get_html_help_exe(): """Return HTML Help Workshop executable path (Windows only)""" if os.name == 'nt': @@ -135,8 +164,8 @@ build = self.get_finalized_command('build') sys.path.insert(0, os.path.abspath(build.build_lib)) dirname = self.distribution.get_command_obj('build').build_purelib - self.builder_target_dir = osp.join(dirname, 'spyderlib', 'doc') - + self.builder_target_dir = osp.join(dirname, 'spyder', 'doc') + if not osp.exists(self.builder_target_dir): os.mkdir(self.builder_target_dir) @@ -152,13 +181,13 @@ "location (path with *only* ASCII characters).", file=sys.stderr) sys.path.pop(0) - + # Building chm doc, if HTML Help Workshop is installed if hhc_exe is not None: fname = osp.join(self.builder_target_dir, 'Spyderdoc.chm') subprocess.call('"%s" %s' % (hhc_exe, fname), shell=True) if osp.isfile(fname): - dest = osp.join(dirname, 'spyderlib') + dest = osp.join(dirname, 'spyder') try: shutil.move(fname, dest) except shutil.Error: @@ -171,39 +200,9 @@ 'is not installed', file=sys.stderr) -NAME = 'spyder' -LIBNAME = 'spyderlib' -from spyderlib import __version__, __project_url__ - - -JOINEDARGS = ''.join(sys.argv) -WINDOWS_INSTALLER = 'bdist_wininst' in JOINEDARGS or 'bdist_msi' in JOINEDARGS -TARGET_MATCH = re.search(r'--target-version=([0-9]*)\.([0-9]*)', JOINEDARGS) -if TARGET_MATCH: - TARGET_VERSION = TARGET_MATCH.groups() -else: - TARGET_VERSION = (str(sys.version_info[0]), str(sys.version_info[1])) - - -def get_packages(): - """Return package list""" - if WINDOWS_INSTALLER: - # Adding pyflakes and rope to the package if available in the - # repository (this is not conventional but Spyder really need - # those tools and there is not decent package manager on - # Windows platforms, so...) - import shutil - import atexit - extdir = 'external-py' + TARGET_VERSION[0] - for name in ('rope', 'pyflakes'): - srcdir = osp.join(extdir, name) - if osp.isdir(srcdir): - dstdir = osp.join(LIBNAME, 'utils', 'external', name) - shutil.copytree(srcdir, dstdir) - atexit.register(shutil.rmtree, osp.abspath(dstdir)) - packages = get_subpackages(LIBNAME)+get_subpackages('spyderplugins') - return packages - +#============================================================================== +# Main scripts +#============================================================================== # NOTE: the '[...]_win_post_install.py' script is installed even on non-Windows # platforms due to a bug in pip installation process (see Issue 1158) SCRIPTS = ['%s_win_post_install.py' % NAME] @@ -211,61 +210,95 @@ SCRIPTS.append('spyder3') else: SCRIPTS.append('spyder') + + +#============================================================================== +# Files added to the package +#============================================================================== EXTLIST = ['.mo', '.svg', '.png', '.css', '.html', '.js', '.chm', '.ini', - '.txt', '.rst', '.qss'] + '.txt', '.rst', '.qss', '.ttf', '.json'] if os.name == 'nt': SCRIPTS += ['spyder.bat'] EXTLIST += ['.ico'] -# Adding a message for the Windows installers -WININST_MSG = "" -if WINDOWS_INSTALLER: - WININST_MSG = \ -"""Please uninstall any previous version of Spyder before continue. - -""" - -setup(name=NAME, +#============================================================================== +# Setup arguments +#============================================================================== +setup_args = dict(name=NAME, version=__version__, description='Scientific PYthon Development EnviRonment', - long_description=WININST_MSG + \ -"""Spyder is an interactive Python development environment providing + long_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 PyQt4 or -PySide application: source code editor with syntax highlighting and -code introspection/analysis features, NumPy array editor, dictionary +It also provides ready-to-use pure-Python widgets to your PyQt5 or +PyQt4 application: source code editor with syntax highlighting and +code introspection/analysis features, NumPy array editor, dictionary editor, Python console, etc.""", download_url='%s/files/%s-%s.zip' % (__project_url__, NAME, __version__), - author="Pierre Raybaut", + author="The Spyder Project Contributors", url=__project_url__, license='MIT', - keywords='PyQt4 PySide editor shell console widgets IDE', + keywords='PyQt5 PyQt4 editor shell console widgets IDE', platforms=['any'], packages=get_packages(), package_data={LIBNAME: get_package_data(LIBNAME, EXTLIST), - 'spyderplugins': - get_package_data('spyderplugins', EXTLIST)}, - requires=["rope (>=0.9.2)", "sphinx (>=0.6.0)", "PyQt4 (>=4.4)"], + 'spyder_breakpoints': get_package_data('spyder_breakpoints', EXTLIST), + 'spyder_profiler': get_package_data('spyder_profiler', EXTLIST), + 'spyder_pylint': get_package_data('spyder_pylint', EXTLIST), + 'spyder_io_dcm': get_package_data('spyder_io_dcm', EXTLIST), + 'spyder_io_hdf5': get_package_data('spyder_io_hdf5', EXTLIST), + }, scripts=[osp.join('scripts', fname) for fname in SCRIPTS], data_files=get_data_files(), - options={"bdist_wininst": - {"install_script": "%s_win_post_install.py" % NAME, - "title": "%s %s" % (NAME.capitalize(), __version__), - "bitmap": osp.join('img_src', 'spyder-bdist_wininst.bmp'), - "target_version": '%s.%s' % TARGET_VERSION, - "user_access_control": "auto"}, - "bdist_msi": - {"install_script": "%s_win_post_install.py" % NAME}}, classifiers=['License :: OSI Approved :: MIT License', 'Operating System :: MacOS', 'Operating System :: Microsoft :: Windows', - 'Operating System :: OS Independent', - 'Operating System :: POSIX', - 'Operating System :: Unix', + 'Operating System :: POSIX :: Linux', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Development Status :: 5 - Production/Stable', 'Topic :: Scientific/Engineering', 'Topic :: Software Development :: Widget Sets'], cmdclass=CMDCLASS) + + +#============================================================================== +# Setuptools deps +#============================================================================== +if any(arg == 'bdist_wheel' for arg in sys.argv): + import setuptools # analysis:ignore + +install_requires = [ + 'rope_py3k' if PY3 else 'rope>=0.9.4', + 'jedi', + 'pyflakes', + 'pygments>=2.0', + 'qtconsole>=4.2.0', + 'nbconvert', + 'sphinx', + 'pep8', + 'pylint', + 'psutil', + 'qtawesome', + 'qtpy>=1.1.0', + 'pickleshare', + 'pyzmq' +] + +if 'setuptools' in sys.modules: + setup_args['install_requires'] = install_requires + + setup_args['entry_points'] = { + 'gui_scripts': [ + 'spyder = spyder.app.start:main' + ] + } + + setup_args.pop('scripts', None) + + +#============================================================================== +# Main setup +#============================================================================== +setup(**setup_args) diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/app/cli_options.py spyder-3.0.2+dfsg1/spyder/app/cli_options.py --- spyder-2.3.8+dfsg1/spyder/app/cli_options.py 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/app/cli_options.py 2016-10-25 02:05:22.000000000 +0200 @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +# +# Copyright © Spyder Project Contributors +# Licensed under the terms of the MIT License +# (see spyder/__init__.py for details) + +import optparse + +def get_options(): + """ + Convert options into commands + return commands, message + """ + parser = optparse.OptionParser(usage="spyder [options] files") + parser.add_option('--new-instance', action='store_true', default=False, + help="Run a new instance of Spyder, even if the single " + "instance mode has been turned on (default)") + parser.add_option('--defaults', dest="reset_to_defaults", + action='store_true', default=False, + help="Reset configuration settings to defaults") + parser.add_option('--reset', dest="reset_config_files", + action='store_true', default=False, + help="Remove all configuration files!") + parser.add_option('--optimize', action='store_true', default=False, + help="Optimize Spyder bytecode (this may require " + "administrative privileges)") + parser.add_option('-w', '--workdir', dest="working_directory", default=None, + help="Default working directory") + parser.add_option('--show-console', action='store_true', default=False, + help="Do not hide parent console window (Windows)") + parser.add_option('--multithread', dest="multithreaded", + action='store_true', default=False, + help="Internal console is executed in another thread " + "(separate from main application thread)") + parser.add_option('--profile', action='store_true', default=False, + help="Profile mode (internal test, " + "not related with Python profiling)") + parser.add_option('--window-title', type=str, default=None, + help="String to show in the main window title") + options, args = parser.parse_args() + return options, args diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/app/__init__.py spyder-3.0.2+dfsg1/spyder/app/__init__.py --- spyder-2.3.8+dfsg1/spyder/app/__init__.py 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/app/__init__.py 2016-10-25 02:05:22.000000000 +0200 @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +# +# Copyright © Spyder Project Contributors +# Licensed under the terms of the MIT License +# (see spyder/__init__.py for details) + +""" +spyder.app +========== + +Modules related to starting and restarting the Spyder application +""" diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/app/mac_stylesheet.qss spyder-3.0.2+dfsg1/spyder/app/mac_stylesheet.qss --- spyder-2.3.8+dfsg1/spyder/app/mac_stylesheet.qss 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/app/mac_stylesheet.qss 2016-10-25 02:05:22.000000000 +0200 @@ -0,0 +1,124 @@ +/* +* Qt Stylesheet for MacOS X +* Copyright (c) Spyder Project Contributors +*/ + + +/* ---------------- Dock widget and QSplitter separators --------------- */ + +QMainWindow::separator { + width: 3px; + height: 3px; + border: 1px solid lightgrey; + border-radius: 1px; +} + +QMainWindow::separator:hover { + background: darkgrey; +} + +QToolButton { + border: none; +} + +QSplitter::handle:horizontal { + border: 1px solid darkgrey; + width: 2px; +} + +QSplitter::handle:vertical { + border: 1px solid darkgrey; + height: 2px; +} + +QSplitter::handle:pressed { + background: darkgrey; +} + + +/* ----------------- Tabs ------------------ */ + +QWidget#tab-container { + background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #b1b1b1, stop: 0.07 #b3b3b3, + stop: 0.33 #b3b3b3, stop: 0.4 #b0b0b0, + stop: 0.47 #b3b3b3, stop: 1.0 #b2b2b2); +} + +QTabWidget::pane#plugin-tab { + border-top: 1px solid qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #b1b1b1, stop: 0.07 #b3b3b3, + stop: 0.33 #b3b3b3, stop: 0.4 #b0b0b0, + stop: 0.47 #b3b3b3, stop: 1.0 #b2b2b2); + border-bottom: 0px; + border-left: 0px; + border-right: 0px; +} + +QTabWidget::tab-bar#plugin-tab { + left: 5px; +} + +QTabBar::tab#plugin-tab { + background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #b1b1b1, stop: 0.07 #b3b3b3, + stop: 0.33 #b3b3b3, stop: 0.4 #b0b0b0, + stop: 0.47 #b3b3b3, stop: 1.0 #b2b2b2); + border: 1px solid #787878; + border-top-color: transparent; + border-bottom-color: transparent; + margin-left: -1px; + margin-right: -1px; + min-width: 15ex; + padding: 3px; +} + +QTabBar::tab:selected#plugin-tab { + background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #dfdfdf, stop: 0.1 #dddddd, + stop: 0.12 #dfdfdf, stop: 0.22 #e0e0e0, + stop: 0.33 #dedede, stop: 0.47 #dedede, + stop: 0.49 #e0e0e0, stop: 0.59 #dddddd, + stop: 0.61 #dfdfdf, stop: 0.73 #dedede, + stop: 0.80 #e0e0e0, stop: 1.0 #dedede); + border: 1px solid #787878; + border-top: 0px; + border-top-color: transparent; + border-bottom-left-radius: 3px; + border-bottom-right-radius: 3px; +} + +QTabBar::tab:first#plugin-tab { + margin-left: 0; +} + +QTabBar::tab:last#plugin-tab { + margin-right: 0; +} + +QTabBar::tab:only-one#plugin-tab { + margin: 0; +} + +QTabBar::scroller#plugin-tab { + width: 22px; +} + +QTabBar#plugin-tab QToolButton::left-arrow { + background: lightgrey; + border-right: 1px solid darkgrey; + image: url($IMAGE_PATH/chevron-left.png); +} + +QTabBar#plugin-tab QToolButton::right-arrow { + background: lightgrey; + image: url($IMAGE_PATH/chevron-right.png); +} + + +/* ------------------ Dock widgets ------------------- */ + +QDockWidget::close-button, QDockWidget::float-button { + padding: 0px; + margin: 2px; +} diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/app/mainwindow.py spyder-3.0.2+dfsg1/spyder/app/mainwindow.py --- spyder-2.3.8+dfsg1/spyder/app/mainwindow.py 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/app/mainwindow.py 2016-11-17 04:39:39.000000000 +0100 @@ -0,0 +1,3016 @@ +# -*- coding: utf-8 -*- +# +# Copyright © Spyder Project Contributors +# Licensed under the terms of the MIT License +# (see spyder/__init__.py for details) + +""" +Spyder, the Scientific PYthon Development EnviRonment +===================================================== + +Developped and maintained by the Spyder Project +Contributors + +Copyright © Spyder Project Contributors +Licensed under the terms of the MIT License +(see spyder/__init__.py for details) +""" + +# ============================================================================= +# Stdlib imports +# ============================================================================= +from __future__ import print_function + +import atexit +import errno +import os +import os.path as osp +import re +import shutil +import signal +import socket +import subprocess +import sys +import threading +import traceback + + +#============================================================================== +# Keeping a reference to the original sys.exit before patching it +#============================================================================== +ORIGINAL_SYS_EXIT = sys.exit + + +#============================================================================== +# Check requirements +#============================================================================== +from spyder import requirements +requirements.check_path() +requirements.check_qt() + + +#============================================================================== +# Windows only: support for hiding console window when started with python.exe +#============================================================================== +set_attached_console_visible = None +is_attached_console_visible = None +set_windows_appusermodelid = None +if os.name == 'nt': + from spyder.utils.windows import (set_attached_console_visible, + is_attached_console_visible, + set_windows_appusermodelid) + + +#============================================================================== +# Workaround: importing rope.base.project here, otherwise this module can't +# be imported if Spyder was executed from another folder than spyder +#============================================================================== +try: + import rope.base.project # analysis:ignore +except ImportError: + pass + + +#============================================================================== +# Qt imports +#============================================================================== +from qtpy import API, PYQT5 +from qtpy.compat import from_qvariant +from qtpy.QtCore import (QByteArray, QCoreApplication, QPoint, QSize, Qt, + QThread, QTimer, QUrl, Signal, Slot) +from qtpy.QtGui import QColor, QDesktopServices, QKeySequence, QPixmap +from qtpy.QtWidgets import (QAction, QApplication, QDockWidget, QMainWindow, + QMenu, QMessageBox, QShortcut, QSplashScreen, + QStyleFactory) +# Avoid a "Cannot mix incompatible Qt library" error on Windows platforms +# when PySide is selected by the QT_API environment variable and when PyQt4 +# is also installed (or any other Qt-based application prepending a directory +# containing incompatible Qt DLLs versions in PATH): +from qtpy import QtSvg # analysis:ignore + +# Avoid a bug in Qt: https://bugreports.qt.io/browse/QTBUG-46720 +from qtpy import QtWebEngineWidgets # analysis:ignore + + +#============================================================================== +# Proper high DPI scaling is available in Qt >= 5.6.0. This attibute must +# be set before creating the application. +#============================================================================== +from spyder.config.main import CONF +if CONF.get('main', 'high_dpi_scaling'): + high_dpi_scaling = True +else: + high_dpi_scaling = False +if hasattr(Qt, 'AA_EnableHighDpiScaling'): + QCoreApplication.setAttribute(Qt.AA_EnableHighDpiScaling, high_dpi_scaling) + + +#============================================================================== +# Create our QApplication instance here because it's needed to render the +# splash screen created below +#============================================================================== +from spyder.utils.qthelpers import qapplication +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() + + +#============================================================================== +# Local utility imports +#============================================================================== +from spyder import __version__, __project_url__, __forum_url__, get_versions +from spyder.config.base import (get_conf_path, get_module_data_path, + get_module_source_path, STDERR, DEBUG, + debug_print, MAC_APP_NAME, get_home_dir, + running_in_mac_app, get_module_path, + reset_config_files) +from spyder.config.main import OPEN_FILES_PORT +from spyder.config.utils import IMPORT_EXT, is_gtk_desktop +from spyder.app.cli_options import get_options +from spyder import dependencies +from spyder.config.ipython import QTCONSOLE_INSTALLED +from spyder.py3compat import (getcwd, is_text_string, to_text_string, + PY3, qbytearray_to_str, configparser as cp) +from spyder.utils import encoding, programs +from spyder.utils import icon_manager as ima +from spyder.utils.introspection import module_completion +from spyder.utils.programs import is_module_installed +from spyder.utils.misc import select_port + +#============================================================================== +# Local gui imports +#============================================================================== +# NOTE: Move (if possible) import's of widgets and plugins exactly where they +# are needed in MainWindow to speed up perceived startup time (i.e. the time +# from clicking the Spyder icon to showing the splash screen). +try: + from spyder.utils.environ import WinUserEnvDialog +except ImportError: + WinUserEnvDialog = None # analysis:ignore + +from spyder.utils.qthelpers import (create_action, add_actions, get_icon, + add_shortcut_to_tooltip, + create_module_bookmark_actions, + create_program_action, DialogManager, + create_python_script_action, file_uri) +from spyder.config.gui import get_shortcut +from spyder.otherplugins import get_spyderplugins_mods +from spyder.app import tour + + +#============================================================================== +# Get the cwd before initializing WorkingDirectory, which sets it to the one +# used in the last session +#============================================================================== +CWD = getcwd() + + +#============================================================================== +# Spyder's main window widgets utilities +#============================================================================== +def get_python_doc_path(): + """ + Return Python documentation path + (Windows: return the PythonXX.chm path if available) + """ + if os.name == 'nt': + doc_path = osp.join(sys.prefix, "Doc") + if not osp.isdir(doc_path): + return + python_chm = [path for path in os.listdir(doc_path) + if re.match(r"(?i)Python[0-9]{3,6}.chm", path)] + if python_chm: + return file_uri(osp.join(doc_path, python_chm[0])) + else: + vinf = sys.version_info + doc_path = '/usr/share/doc/python%d.%d/html' % (vinf[0], vinf[1]) + python_doc = osp.join(doc_path, "index.html") + if osp.isfile(python_doc): + return file_uri(python_doc) + + +def get_focus_python_shell(): + """Extract and return Python shell from widget + Return None if *widget* is not a Python shell (e.g. IPython kernel)""" + widget = QApplication.focusWidget() + from spyder.widgets.shell import PythonShellWidget + from spyder.widgets.externalshell.pythonshell import ExternalPythonShell + if isinstance(widget, PythonShellWidget): + return widget + elif isinstance(widget, ExternalPythonShell): + return widget.shell + + +def get_focus_widget_properties(): + """Get properties of focus widget + Returns tuple (widget, properties) where properties is a tuple of + booleans: (is_console, not_readonly, readwrite_editor)""" + widget = QApplication.focusWidget() + from spyder.widgets.shell import ShellBaseWidget + from spyder.widgets.editor import TextEditBaseWidget + textedit_properties = None + if isinstance(widget, (ShellBaseWidget, TextEditBaseWidget)): + console = isinstance(widget, ShellBaseWidget) + not_readonly = not widget.isReadOnly() + readwrite_editor = not_readonly and not console + textedit_properties = (console, not_readonly, readwrite_editor) + return widget, textedit_properties + + +#============================================================================== +# Main Window +#============================================================================== +class MainWindow(QMainWindow): + """Spyder main window""" + DOCKOPTIONS = QMainWindow.AllowTabbedDocks|QMainWindow.AllowNestedDocks + SPYDER_PATH = get_conf_path('path') + BOOKMARKS = ( + ('numpy', "http://docs.scipy.org/doc/", + _("Numpy and Scipy documentation")), + ('matplotlib', "http://matplotlib.sourceforge.net/contents.html", + _("Matplotlib documentation")), + ('PyQt4', + "http://pyqt.sourceforge.net/Docs/PyQt4/", + _("PyQt4 Reference Guide")), + ('PyQt4', + "http://pyqt.sourceforge.net/Docs/PyQt4/classes.html", + _("PyQt4 API Reference")), + ('xy', "http://code.google.com/p/pythonxy/", + _("Python(x,y)")), + ('winpython', "https://winpython.github.io/", + _("WinPython")) + ) + + # Signals + restore_scrollbar_position = Signal() + all_actions_defined = Signal() + sig_pythonpath_changed = Signal() + sig_open_external_file = Signal(str) + sig_resized = Signal("QResizeEvent") # related to interactive tour + sig_moved = Signal("QMoveEvent") # related to interactive tour + + def __init__(self, options=None): + QMainWindow.__init__(self) + + qapp = QApplication.instance() + if PYQT5: + # Enabling scaling for high dpi + qapp.setAttribute(Qt.AA_UseHighDpiPixmaps) + self.default_style = str(qapp.style().objectName()) + + self.dialog_manager = DialogManager() + + self.init_workdir = options.working_directory + self.profile = options.profile + self.multithreaded = options.multithreaded + self.new_instance = options.new_instance + + self.debug_print("Start of MainWindow constructor") + + def signal_handler(signum, frame=None): + """Handler for signals.""" + sys.stdout.write('Handling signal: %s\n' % signum) + sys.stdout.flush() + QApplication.quit() + + if os.name == "nt": + try: + import win32api + win32api.SetConsoleCtrlHandler(signal_handler, True) + except ImportError: + pass + else: + signal.signal(signal.SIGTERM, signal_handler) + + # Use a custom Qt stylesheet + if sys.platform == 'darwin': + spy_path = get_module_source_path('spyder') + img_path = osp.join(spy_path, 'images') + mac_style = open(osp.join(spy_path, 'app', 'mac_stylesheet.qss')).read() + mac_style = mac_style.replace('$IMAGE_PATH', img_path) + self.setStyleSheet(mac_style) + + # Shortcut management data + self.shortcut_data = [] + + # Loading Spyder path + self.path = [] + self.project_path = [] + if osp.isfile(self.SPYDER_PATH): + self.path, _x = encoding.readlines(self.SPYDER_PATH) + self.path = [name for name in self.path if osp.isdir(name)] + self.remove_path_from_sys_path() + self.add_path_to_sys_path() + + # Plugins + self.console = None + self.workingdirectory = None + self.editor = None + self.explorer = None + self.help = None + self.onlinehelp = None + self.projects = None + self.outlineexplorer = None + self.historylog = None + self.extconsole = None + self.ipyconsole = None + self.variableexplorer = None + self.findinfiles = None + self.thirdparty_plugins = [] + + # Tour # TODO: Should I consider it a plugin?? or? + self.tour = None + self.tours_available = None + + # Check for updates Thread and Worker, refereces needed to prevent + # segfaulting + self.check_updates_action = None + self.thread_updates = None + self.worker_updates = None + self.give_updates_feedback = True + + # Preferences + from spyder.plugins.configdialog import (MainConfigPage, + ColorSchemeConfigPage) + from spyder.plugins.shortcuts import ShortcutsConfigPage + from spyder.plugins.runconfig import RunConfigPage + from spyder.plugins.maininterpreter import MainInterpreterConfigPage + self.general_prefs = [MainConfigPage, ShortcutsConfigPage, + ColorSchemeConfigPage, MainInterpreterConfigPage, + RunConfigPage] + self.prefs_index = None + self.prefs_dialog_size = None + + # Quick Layouts and Dialogs + from spyder.plugins.layoutdialog import (LayoutSaveDialog, + LayoutSettingsDialog) + self.dialog_layout_save = LayoutSaveDialog + self.dialog_layout_settings = LayoutSettingsDialog + + # Actions + self.lock_dockwidgets_action = None + self.show_toolbars_action = None + self.close_dockwidget_action = None + self.undo_action = None + self.redo_action = None + self.copy_action = None + self.cut_action = None + self.paste_action = None + self.selectall_action = None + self.maximize_action = None + self.fullscreen_action = None + + # Menu bars + self.file_menu = None + self.file_menu_actions = [] + self.edit_menu = None + self.edit_menu_actions = [] + self.search_menu = None + self.search_menu_actions = [] + self.source_menu = None + self.source_menu_actions = [] + self.run_menu = None + self.run_menu_actions = [] + self.debug_menu = None + self.debug_menu_actions = [] + self.consoles_menu = None + self.consoles_menu_actions = [] + self.projects_menu = None + self.projects_menu_actions = [] + self.tools_menu = None + self.tools_menu_actions = [] + self.external_tools_menu = None # We must keep a reference to this, + # otherwise the external tools menu is lost after leaving setup method + self.external_tools_menu_actions = [] + self.view_menu = None + self.plugins_menu = None + self.plugins_menu_actions = [] + self.toolbars_menu = None + self.help_menu = None + self.help_menu_actions = [] + + # Status bar widgets + self.mem_status = None + self.cpu_status = None + + # Toolbars + self.visible_toolbars = [] + self.toolbarslist = [] + self.main_toolbar = None + self.main_toolbar_actions = [] + self.file_toolbar = None + self.file_toolbar_actions = [] + self.edit_toolbar = None + self.edit_toolbar_actions = [] + self.search_toolbar = None + self.search_toolbar_actions = [] + self.source_toolbar = None + self.source_toolbar_actions = [] + self.run_toolbar = None + self.run_toolbar_actions = [] + self.debug_toolbar = None + self.debug_toolbar_actions = [] + self.layout_toolbar = None + self.layout_toolbar_actions = [] + + + # Set Window title and icon + if DEV is not None: + title = "Spyder %s (Python %s.%s)" % (__version__, + sys.version_info[0], + sys.version_info[1]) + else: + title = "Spyder (Python %s.%s)" % (sys.version_info[0], + sys.version_info[1]) + if DEBUG: + title += " [DEBUG MODE %d]" % DEBUG + if options.window_title is not None: + title += ' -- ' + options.window_title + + self.base_title = title + self.update_window_title() + resample = os.name != 'nt' + icon = ima.icon('spyder', resample=resample) + # Resampling SVG icon only on non-Windows platforms (see Issue 1314): + self.setWindowIcon(icon) + if set_windows_appusermodelid != None: + res = set_windows_appusermodelid() + debug_print("appusermodelid: " + str(res)) + + # Setting QTimer if running in travis + test_travis = os.environ.get('TEST_CI_APP', None) + if test_travis is not None: + global MAIN_APP + timer_shutdown_time = 30000 + self.timer_shutdown = QTimer(self) + self.timer_shutdown.timeout.connect(MAIN_APP.quit) + self.timer_shutdown.start(timer_shutdown_time) + + # Showing splash screen + self.splash = SPLASH + if CONF.get('main', 'current_version', '') != __version__: + CONF.set('main', 'current_version', __version__) + # Execute here the actions to be performed only once after + # each update (there is nothing there for now, but it could + # be useful some day...) + + # List of satellite widgets (registered in add_dockwidget): + self.widgetlist = [] + + # Flags used if closing() is called by the exit() shell command + self.already_closed = False + self.is_starting_up = True + self.is_setting_up = True + + self.dockwidgets_locked = CONF.get('main', 'panes_locked') + self.floating_dockwidgets = [] + self.window_size = None + self.window_position = None + self.state_before_maximizing = None + self.current_quick_layout = None + self.previous_layout_settings = None # TODO: related to quick layouts + self.last_plugin = None + self.fullscreen_flag = None # isFullscreen does not work as expected + # The following flag remember the maximized state even when + # the window is in fullscreen mode: + self.maximized_flag = None + + # Track which console plugin type had last focus + # True: Console plugin + # False: IPython console plugin + self.last_console_plugin_focus_was_python = True + + # To keep track of the last focused widget + self.last_focused_widget = None + + # Server to open external files on a single instance + self.open_files_server = socket.socket(socket.AF_INET, + socket.SOCK_STREAM, + socket.IPPROTO_TCP) + + self.apply_settings() + self.debug_print("End of MainWindow constructor") + + def debug_print(self, message): + """Debug prints""" + debug_print(message) + + #---- Window setup + def create_toolbar(self, title, object_name, iconsize=24): + """Create and return toolbar with *title* and *object_name*""" + toolbar = self.addToolBar(title) + toolbar.setObjectName(object_name) + toolbar.setIconSize(QSize(iconsize, iconsize)) + self.toolbarslist.append(toolbar) + return toolbar + + def setup(self): + """Setup main window""" + self.debug_print("*** Start of MainWindow setup ***") + self.debug_print(" ..core actions") + self.close_dockwidget_action = create_action(self, + icon=ima.icon('DialogCloseButton'), + text=_("Close current pane"), + triggered=self.close_current_dockwidget, + context=Qt.ApplicationShortcut) + self.register_shortcut(self.close_dockwidget_action, "_", + "Close pane") + self.lock_dockwidgets_action = create_action(self, _("Lock panes"), + toggled=self.toggle_lock_dockwidgets, + context=Qt.ApplicationShortcut) + self.register_shortcut(self.lock_dockwidgets_action, "_", + "Lock unlock panes") + # custom layouts shortcuts + self.toggle_next_layout_action = create_action(self, + _("Use next layout"), + triggered=self.toggle_next_layout, + context=Qt.ApplicationShortcut) + self.toggle_previous_layout_action = create_action(self, + _("Use previous layout"), + triggered=self.toggle_previous_layout, + context=Qt.ApplicationShortcut) + self.register_shortcut(self.toggle_next_layout_action, "_", + "Use next layout") + self.register_shortcut(self.toggle_previous_layout_action, "_", + "Use previous layout") + + def create_edit_action(text, tr_text, icon): + textseq = text.split(' ') + method_name = textseq[0].lower()+"".join(textseq[1:]) + action = create_action(self, tr_text, + icon=icon, + triggered=self.global_callback, + data=method_name, + context=Qt.WidgetShortcut) + self.register_shortcut(action, "Editor", text) + return action + + self.undo_action = create_edit_action('Undo', _('Undo'), + ima.icon('undo')) + self.redo_action = create_edit_action('Redo', _('Redo'), + ima.icon('redo')) + self.copy_action = create_edit_action('Copy', _('Copy'), + ima.icon('editcopy')) + self.cut_action = create_edit_action('Cut', _('Cut'), + ima.icon('editcut')) + self.paste_action = create_edit_action('Paste', _('Paste'), + ima.icon('editpaste')) + self.selectall_action = create_edit_action("Select All", + _("Select All"), + ima.icon('selectall')) + + self.edit_menu_actions = [self.undo_action, self.redo_action, + None, self.cut_action, self.copy_action, + self.paste_action, self.selectall_action] + + namespace = None + self.debug_print(" ..toolbars") + # File menu/toolbar + self.file_menu = self.menuBar().addMenu(_("&File")) + self.file_toolbar = self.create_toolbar(_("File toolbar"), + "file_toolbar") + + # Edit menu/toolbar + self.edit_menu = self.menuBar().addMenu(_("&Edit")) + self.edit_toolbar = self.create_toolbar(_("Edit toolbar"), + "edit_toolbar") + + # Search menu/toolbar + self.search_menu = self.menuBar().addMenu(_("&Search")) + self.search_toolbar = self.create_toolbar(_("Search toolbar"), + "search_toolbar") + + # Source menu/toolbar + self.source_menu = self.menuBar().addMenu(_("Sour&ce")) + self.source_toolbar = self.create_toolbar(_("Source toolbar"), + "source_toolbar") + + # Run menu/toolbar + self.run_menu = self.menuBar().addMenu(_("&Run")) + self.run_toolbar = self.create_toolbar(_("Run toolbar"), + "run_toolbar") + + # Debug menu/toolbar + self.debug_menu = self.menuBar().addMenu(_("&Debug")) + self.debug_toolbar = self.create_toolbar(_("Debug toolbar"), + "debug_toolbar") + + # Consoles menu/toolbar + self.consoles_menu = self.menuBar().addMenu(_("C&onsoles")) + + # Projects menu + self.projects_menu = self.menuBar().addMenu(_("&Projects")) + + # Tools menu + self.tools_menu = self.menuBar().addMenu(_("&Tools")) + + # View menu + self.view_menu = self.menuBar().addMenu(_("&View")) + + # Help menu + self.help_menu = self.menuBar().addMenu(_("&Help")) + + # Status bar + status = self.statusBar() + status.setObjectName("StatusBar") + status.showMessage(_("Welcome to Spyder!"), 5000) + + + self.debug_print(" ..tools") + # Tools + External Tools + prefs_action = create_action(self, _("Pre&ferences"), + icon=ima.icon('configure'), + triggered=self.edit_preferences, + context=Qt.ApplicationShortcut) + self.register_shortcut(prefs_action, "_", "Preferences", + add_sc_to_tip=True) + spyder_path_action = create_action(self, + _("PYTHONPATH manager"), + None, icon=ima.icon('pythonpath'), + triggered=self.path_manager_callback, + tip=_("Python Path Manager"), + menurole=QAction.ApplicationSpecificRole) + update_modules_action = create_action(self, + _("Update module names list"), + triggered=lambda: + module_completion.reset(), + tip=_("Refresh list of module names " + "available in PYTHONPATH")) + reset_spyder_action = create_action( + self, _("Reset Spyder to factory defaults"), + triggered=self.reset_spyder) + self.tools_menu_actions = [prefs_action, spyder_path_action] + if WinUserEnvDialog is not None: + winenv_action = create_action(self, + _("Current user environment variables..."), + icon='win_env.png', + tip=_("Show and edit current user environment " + "variables in Windows registry " + "(i.e. for all sessions)"), + triggered=self.win_env) + self.tools_menu_actions.append(winenv_action) + self.tools_menu_actions += [reset_spyder_action, None, + update_modules_action] + + # External Tools submenu + self.external_tools_menu = QMenu(_("External Tools")) + self.external_tools_menu_actions = [] + # Python(x,y) launcher + self.xy_action = create_action(self, + _("Python(x,y) launcher"), + icon=get_icon('pythonxy.png'), + triggered=lambda: + programs.run_python_script('xy', 'xyhome')) + if os.name == 'nt' and is_module_installed('xy'): + self.external_tools_menu_actions.append(self.xy_action) + # WinPython control panel + self.wp_action = create_action(self, _("WinPython control panel"), + icon=get_icon('winpython.svg'), + triggered=lambda: + programs.run_python_script('winpython', 'controlpanel')) + if os.name == 'nt' and is_module_installed('winpython'): + self.external_tools_menu_actions.append(self.wp_action) + # Qt-related tools + additact = [] + for name in ("designer-qt4", "designer"): + qtdact = create_program_action(self, _("Qt Designer"), + name, 'qtdesigner.png') + if qtdact: + break + for name in ("linguist-qt4", "linguist"): + qtlact = create_program_action(self, _("Qt Linguist"), + "linguist", 'qtlinguist.png') + if qtlact: + break + args = ['-no-opengl'] if os.name == 'nt' else [] + qteact = create_python_script_action(self, + _("Qt examples"), 'qt.png', "PyQt4", + osp.join("examples", "demos", + "qtdemo", "qtdemo"), args) + for act in (qtdact, qtlact, qteact): + if act: + additact.append(act) + if additact and (is_module_installed('winpython') or \ + is_module_installed('xy')): + self.external_tools_menu_actions += [None] + additact + + # Guidata and Sift + self.debug_print(" ..sift?") + gdgq_act = [] + # Guidata and Guiqwt don't support PyQt5 yet and they fail + # with an AssertionError when imported using those bindings + # (see issue 2274) + try: + from guidata import configtools + from guidata import config # analysis:ignore + guidata_icon = configtools.get_icon('guidata.svg') + guidata_act = create_python_script_action(self, + _("guidata examples"), guidata_icon, + "guidata", + osp.join("tests", "__init__")) + gdgq_act += [guidata_act] + except (ImportError, AssertionError): + pass + try: + from guidata import configtools + from guiqwt import config # analysis:ignore + guiqwt_icon = configtools.get_icon('guiqwt.svg') + guiqwt_act = create_python_script_action(self, + _("guiqwt examples"), guiqwt_icon, "guiqwt", + osp.join("tests", "__init__")) + if guiqwt_act: + gdgq_act += [guiqwt_act] + sift_icon = configtools.get_icon('sift.svg') + sift_act = create_python_script_action(self, _("Sift"), + sift_icon, "guiqwt", osp.join("tests", "sift")) + if sift_act: + gdgq_act += [sift_act] + except (ImportError, AssertionError): + pass + if gdgq_act: + self.external_tools_menu_actions += [None] + gdgq_act + + # ViTables + vitables_act = create_program_action(self, _("ViTables"), + "vitables", 'vitables.png') + if vitables_act: + self.external_tools_menu_actions += [None, vitables_act] + + # Maximize current plugin + self.maximize_action = create_action(self, '', + triggered=self.maximize_dockwidget, + context=Qt.ApplicationShortcut) + self.register_shortcut(self.maximize_action, "_", "Maximize pane") + self.__update_maximize_action() + + # Fullscreen mode + self.fullscreen_action = create_action(self, + _("Fullscreen mode"), + triggered=self.toggle_fullscreen, + context=Qt.ApplicationShortcut) + self.register_shortcut(self.fullscreen_action, "_", + "Fullscreen mode", add_sc_to_tip=True) + + # Main toolbar + self.main_toolbar_actions = [self.maximize_action, + self.fullscreen_action, + None, + prefs_action, spyder_path_action] + + self.main_toolbar = self.create_toolbar(_("Main toolbar"), + "main_toolbar") + + # Internal console plugin + self.debug_print(" ..plugin: internal console") + from spyder.plugins.console import Console + self.console = Console(self, namespace, exitfunc=self.closing, + profile=self.profile, + multithreaded=self.multithreaded, + message=_("Spyder Internal Console\n\n" + "This console is used to report application\n" + "internal errors and to inspect Spyder\n" + "internals with the following commands:\n" + " spy.app, spy.window, dir(spy)\n\n" + "Please don't use it to run your code\n\n")) + self.console.register_plugin() + + # Working directory plugin + self.debug_print(" ..plugin: working directory") + from spyder.plugins.workingdirectory import WorkingDirectory + self.workingdirectory = WorkingDirectory(self, self.init_workdir, main=self) + self.workingdirectory.register_plugin() + self.toolbarslist.append(self.workingdirectory) + + # Help plugin + if CONF.get('help', 'enable'): + self.set_splash(_("Loading help...")) + from spyder.plugins.help import Help + self.help = Help(self) + self.help.register_plugin() + + # Outline explorer widget + if CONF.get('outline_explorer', 'enable'): + self.set_splash(_("Loading outline explorer...")) + from spyder.plugins.outlineexplorer import OutlineExplorer + fullpath_sorting = CONF.get('editor', 'fullpath_sorting', True) + self.outlineexplorer = OutlineExplorer(self, + fullpath_sorting=fullpath_sorting) + self.outlineexplorer.register_plugin() + + # Editor plugin + self.set_splash(_("Loading editor...")) + from spyder.plugins.editor import Editor + self.editor = Editor(self) + self.editor.register_plugin() + + # Populating file menu entries + quit_action = create_action(self, _("&Quit"), + icon=ima.icon('exit'), + tip=_("Quit"), + triggered=self.console.quit, + context=Qt.ApplicationShortcut) + self.register_shortcut(quit_action, "_", "Quit") + restart_action = create_action(self, _("&Restart"), + icon=ima.icon('restart'), + tip=_("Restart"), + triggered=self.restart, + context=Qt.ApplicationShortcut) + self.register_shortcut(restart_action, "_", "Restart") + + self.file_menu_actions += [None, restart_action, quit_action] + self.set_splash("") + + self.debug_print(" ..widgets") + # Find in files + if CONF.get('find_in_files', 'enable'): + from spyder.plugins.findinfiles import FindInFiles + self.findinfiles = FindInFiles(self) + self.findinfiles.register_plugin() + + # Explorer + if CONF.get('explorer', 'enable'): + self.set_splash(_("Loading file explorer...")) + from spyder.plugins.explorer import Explorer + self.explorer = Explorer(self) + self.explorer.register_plugin() + + # History log widget + if CONF.get('historylog', 'enable'): + self.set_splash(_("Loading history plugin...")) + from spyder.plugins.history import HistoryLog + self.historylog = HistoryLog(self) + self.historylog.register_plugin() + + # Online help widget + try: # Qt >= v4.4 + from spyder.plugins.onlinehelp import OnlineHelp + except ImportError: # Qt < v4.4 + OnlineHelp = None # analysis:ignore + if CONF.get('onlinehelp', 'enable') and OnlineHelp is not None: + self.set_splash(_("Loading online help...")) + self.onlinehelp = OnlineHelp(self) + self.onlinehelp.register_plugin() + + # Project explorer widget + self.set_splash(_("Loading project explorer...")) + from spyder.plugins.projects import Projects + self.projects = Projects(self) + self.projects.register_plugin() + self.project_path = self.projects.get_pythonpath(at_start=True) + + # External console + self.set_splash(_("Loading external console...")) + from spyder.plugins.externalconsole import ExternalConsole + self.extconsole = ExternalConsole(self) + self.extconsole.register_plugin() + + # Namespace browser + self.set_splash(_("Loading namespace browser...")) + from spyder.plugins.variableexplorer import VariableExplorer + self.variableexplorer = VariableExplorer(self) + self.variableexplorer.register_plugin() + + # IPython console + if QTCONSOLE_INSTALLED: + self.set_splash(_("Loading IPython console...")) + from spyder.plugins.ipythonconsole import IPythonConsole + self.ipyconsole = IPythonConsole(self) + self.ipyconsole.register_plugin() + + self.set_splash(_("Setting up main window...")) + + # Help menu + dep_action = create_action(self, _("Dependencies..."), + triggered=self.show_dependencies, + icon=ima.icon('advanced')) + report_action = create_action(self, + _("Report issue..."), + icon=ima.icon('bug'), + triggered=self.report_issue) + support_action = create_action(self, + _("Spyder support..."), + triggered=self.google_group) + self.check_updates_action = create_action(self, + _("Check for updates..."), + triggered=self.check_updates) + + # Spyder documentation + doc_path = get_module_data_path('spyder', relpath="doc", + attr_name='DOCPATH') + # * Trying to find the chm doc + spyder_doc = osp.join(doc_path, "Spyderdoc.chm") + if not osp.isfile(spyder_doc): + spyder_doc = osp.join(doc_path, os.pardir, "Spyderdoc.chm") + # * Trying to find the html doc + if not osp.isfile(spyder_doc): + spyder_doc = osp.join(doc_path, "index.html") + # * Trying to find the development-version html doc + if not osp.isfile(spyder_doc): + spyder_doc = osp.join(get_module_source_path('spyder'), + os.pardir, 'build', 'lib', 'spyder', + 'doc', "index.html") + # * If we totally fail, point to our web build + if not osp.isfile(spyder_doc): + spyder_doc = 'http://pythonhosted.org/spyder' + else: + spyder_doc = file_uri(spyder_doc) + doc_action = create_action( self, _("Spyder documentation"), shortcut="F1", + icon=ima.icon('DialogHelpButton'), + triggered=lambda : programs.start_file(spyder_doc)) + + if self.help is not None: + tut_action = create_action(self, _("Spyder tutorial"), + triggered=self.help.show_tutorial) + else: + tut_action = None + + #----- Tours + self.tour = tour.AnimatedTour(self) + self.tours_menu = QMenu(_("Interactive tours")) + self.tour_menu_actions = [] + # TODO: Only show intro tour for now. When we are close to finish + # 3.0, we will finish and show the other tour + self.tours_available = tour.get_tours(0) + + for i, tour_available in enumerate(self.tours_available): + self.tours_available[i]['last'] = 0 + tour_name = tour_available['name'] + + def trigger(i=i, self=self): # closure needed! + return lambda: self.show_tour(i) + + temp_action = create_action(self, tour_name, tip="", + triggered=trigger()) + self.tour_menu_actions += [temp_action] + + self.tours_menu.addActions(self.tour_menu_actions) + + if not DEV: + self.tours_menu = None + + self.help_menu_actions = [doc_action, tut_action, self.tours_menu, + None, report_action, dep_action, + self.check_updates_action, support_action, + None] + # Python documentation + if get_python_doc_path() is not None: + pydoc_act = create_action(self, _("Python documentation"), + triggered=lambda: + programs.start_file(get_python_doc_path())) + self.help_menu_actions.append(pydoc_act) + # IPython documentation + if self.ipyconsole is not None and self.help is not None: + ipython_menu = QMenu(_("IPython documentation"), self) + intro_action = create_action(self, _("Intro to IPython"), + triggered=self.ipyconsole.show_intro) + quickref_action = create_action(self, _("Quick reference"), + triggered=self.ipyconsole.show_quickref) + guiref_action = create_action(self, _("Console help"), + triggered=self.ipyconsole.show_guiref) + add_actions(ipython_menu, (intro_action, guiref_action, + quickref_action)) + self.help_menu_actions.append(ipython_menu) + # Windows-only: documentation located in sys.prefix/Doc + ipm_actions = [] + def add_ipm_action(text, path): + """Add installed Python module doc action to help submenu""" + # QAction.triggered works differently for PySide and PyQt + path = file_uri(path) + if not API == 'pyside': + slot=lambda _checked, path=path: programs.start_file(path) + else: + slot=lambda path=path: programs.start_file(path) + action = create_action(self, text, + icon='%s.png' % osp.splitext(path)[1][1:], + triggered=slot) + ipm_actions.append(action) + sysdocpth = osp.join(sys.prefix, 'Doc') + if osp.isdir(sysdocpth): # exists on Windows, except frozen dist. + for docfn in os.listdir(sysdocpth): + pt = r'([a-zA-Z\_]*)(doc)?(-dev)?(-ref)?(-user)?.(chm|pdf)' + match = re.match(pt, docfn) + if match is not None: + pname = match.groups()[0] + if pname not in ('Python', ): + add_ipm_action(pname, osp.join(sysdocpth, docfn)) + # Documentation provided by Python(x,y), if available + try: + from xy.config import DOC_PATH as xy_doc_path + xydoc = osp.join(xy_doc_path, "Libraries") + def add_xydoc(text, pathlist): + for path in pathlist: + if osp.exists(path): + add_ipm_action(text, path) + break + add_xydoc(_("Python(x,y) documentation folder"), + [xy_doc_path]) + add_xydoc(_("IPython documentation"), + [osp.join(xydoc, "IPython", "ipythondoc.chm")]) + add_xydoc(_("guidata documentation"), + [osp.join(xydoc, "guidata", "guidatadoc.chm"), + r"D:\Python\guidata\build\doc_chm\guidatadoc.chm"]) + add_xydoc(_("guiqwt documentation"), + [osp.join(xydoc, "guiqwt", "guiqwtdoc.chm"), + r"D:\Python\guiqwt\build\doc_chm\guiqwtdoc.chm"]) + add_xydoc(_("Matplotlib documentation"), + [osp.join(xydoc, "matplotlib", "Matplotlibdoc.chm"), + osp.join(xydoc, "matplotlib", "Matplotlib.pdf")]) + add_xydoc(_("NumPy documentation"), + [osp.join(xydoc, "NumPy", "numpy.chm")]) + add_xydoc(_("NumPy reference guide"), + [osp.join(xydoc, "NumPy", "numpy-ref.pdf")]) + add_xydoc(_("NumPy user guide"), + [osp.join(xydoc, "NumPy", "numpy-user.pdf")]) + add_xydoc(_("SciPy documentation"), + [osp.join(xydoc, "SciPy", "scipy.chm"), + osp.join(xydoc, "SciPy", "scipy-ref.pdf")]) + except (ImportError, KeyError, RuntimeError): + pass + # Installed Python modules submenu (Windows only) + if ipm_actions: + pymods_menu = QMenu(_("Installed Python modules"), self) + add_actions(pymods_menu, ipm_actions) + self.help_menu_actions.append(pymods_menu) + # Online documentation + web_resources = QMenu(_("Online documentation")) + webres_actions = create_module_bookmark_actions(self, + self.BOOKMARKS) + webres_actions.insert(2, None) + webres_actions.insert(5, None) + add_actions(web_resources, webres_actions) + self.help_menu_actions.append(web_resources) + # Qt assistant link + if sys.platform.startswith('linux') and not PYQT5: + qta_exe = "assistant-qt4" + else: + qta_exe = "assistant" + qta_act = create_program_action(self, _("Qt documentation"), + qta_exe) + if qta_act: + self.help_menu_actions += [qta_act, None] + # About Spyder + about_action = create_action(self, + _("About %s...") % "Spyder", + icon=ima.icon('MessageBoxInformation'), + triggered=self.about) + self.help_menu_actions += [None, about_action] + + # Status bar widgets + from spyder.widgets.status import MemoryStatus, CPUStatus + self.mem_status = MemoryStatus(self, status) + self.cpu_status = CPUStatus(self, status) + self.apply_statusbar_settings() + + # Third-party plugins + for mod in get_spyderplugins_mods(): + try: + plugin = mod.PLUGIN_CLASS(self) + self.thirdparty_plugins.append(plugin) + plugin.register_plugin() + except Exception as error: + print("%s: %s" % (mod, str(error)), file=STDERR) + traceback.print_exc(file=STDERR) + + + #----- View + # View menu + self.plugins_menu = QMenu(_("Panes"), self) + + self.toolbars_menu = QMenu(_("Toolbars"), self) + self.quick_layout_menu = QMenu(_("Window layouts"), self) + self.quick_layout_set_menu() + + self.view_menu.addMenu(self.plugins_menu) # Panes + add_actions(self.view_menu, (self.lock_dockwidgets_action, + self.close_dockwidget_action, + self.maximize_action, + None)) + self.show_toolbars_action = create_action(self, + _("Show toolbars"), + triggered=self.show_toolbars, + context=Qt.ApplicationShortcut) + self.register_shortcut(self.show_toolbars_action, "_", + "Show toolbars") + self.view_menu.addMenu(self.toolbars_menu) + self.view_menu.addAction(self.show_toolbars_action) + add_actions(self.view_menu, (None, + self.quick_layout_menu, + self.toggle_previous_layout_action, + self.toggle_next_layout_action, + None, + self.fullscreen_action)) + if set_attached_console_visible is not None: + cmd_act = create_action(self, + _("Attached console window (debugging)"), + toggled=set_attached_console_visible) + cmd_act.setChecked(is_attached_console_visible()) + add_actions(self.view_menu, (None, cmd_act)) + + # Adding external tools action to "Tools" menu + if self.external_tools_menu_actions: + external_tools_act = create_action(self, _("External Tools")) + external_tools_act.setMenu(self.external_tools_menu) + self.tools_menu_actions += [None, external_tools_act] + + # Filling out menu/toolbar entries: + add_actions(self.file_menu, self.file_menu_actions) + add_actions(self.edit_menu, self.edit_menu_actions) + add_actions(self.search_menu, self.search_menu_actions) + add_actions(self.source_menu, self.source_menu_actions) + add_actions(self.run_menu, self.run_menu_actions) + add_actions(self.debug_menu, self.debug_menu_actions) + add_actions(self.consoles_menu, self.consoles_menu_actions) + add_actions(self.projects_menu, self.projects_menu_actions) + add_actions(self.tools_menu, self.tools_menu_actions) + add_actions(self.external_tools_menu, + self.external_tools_menu_actions) + add_actions(self.help_menu, self.help_menu_actions) + + add_actions(self.main_toolbar, self.main_toolbar_actions) + add_actions(self.file_toolbar, self.file_toolbar_actions) + add_actions(self.edit_toolbar, self.edit_toolbar_actions) + add_actions(self.search_toolbar, self.search_toolbar_actions) + add_actions(self.source_toolbar, self.source_toolbar_actions) + add_actions(self.debug_toolbar, self.debug_toolbar_actions) + add_actions(self.run_toolbar, self.run_toolbar_actions) + + # Apply all defined shortcuts (plugins + 3rd-party plugins) + self.apply_shortcuts() + + # Emitting the signal notifying plugins that main window menu and + # toolbar actions are all defined: + self.all_actions_defined.emit() + + # Window set-up + self.debug_print("Setting up window...") + self.setup_layout(default=False) + + # Show and hide shortcuts in menus for Mac. + # This is a workaround because we can't disable shortcuts + # by setting context=Qt.WidgetShortcut there + if sys.platform == 'darwin': + for name in ['file', 'search', 'source', 'run', 'debug', + 'plugins']: + menu_object = getattr(self, name + '_menu') + menu_object.aboutToShow.connect( + lambda name=name: self.show_shortcuts(name)) + menu_object.aboutToHide.connect( + lambda name=name: self.hide_shortcuts(name)) + + self.splash.hide() + + # Enabling tear off for all menus except help menu + if CONF.get('main', 'tear_off_menus'): + for child in self.menuBar().children(): + if isinstance(child, QMenu) and child != self.help_menu: + child.setTearOffEnabled(True) + + # Menu about to show + for child in self.menuBar().children(): + if isinstance(child, QMenu): + child.aboutToShow.connect(self.update_edit_menu) + + self.debug_print("*** End of MainWindow setup ***") + self.is_starting_up = False + + def post_visible_setup(self): + """Actions to be performed only after the main window's `show` method + was triggered""" + self.restore_scrollbar_position.emit() + + # Remove our temporary dir + atexit.register(self.remove_tmpdir) + + # [Workaround for Issue 880] + # QDockWidget objects are not painted if restored as floating + # windows, so we must dock them before showing the mainwindow, + # then set them again as floating windows here. + for widget in self.floating_dockwidgets: + widget.setFloating(True) + + # In MacOS X 10.7 our app is not displayed after initialized (I don't + # know why because this doesn't happen when started from the terminal), + # so we need to resort to this hack to make it appear. + if running_in_mac_app(): + idx = __file__.index(MAC_APP_NAME) + app_path = __file__[:idx] + subprocess.call(['open', app_path + MAC_APP_NAME]) + + # Server to maintain just one Spyder instance and open files in it if + # the user tries to start other instances with + # $ spyder foo.py + if CONF.get('main', 'single_instance') and not self.new_instance: + t = threading.Thread(target=self.start_open_files_server) + t.setDaemon(True) + t.start() + + # Connect the window to the signal emmited by the previous server + # when it gets a client connected to it + self.sig_open_external_file.connect(self.open_external_file) + + # Create Plugins and toolbars submenus + self.create_plugins_menu() + self.create_toolbars_menu() + + self.extconsole.setMinimumHeight(0) + + # Update toolbar visibility status + self.toolbars_visible = CONF.get('main', 'toolbars_visible') + self.load_last_visible_toolbars() + + # Update lock status of dockidgets (panes) + self.lock_dockwidgets_action.setChecked(self.dockwidgets_locked) + self.apply_panes_settings() + + # Hide Internal Console so that people don't use it instead of + # the External or IPython ones + if self.console.dockwidget.isVisible() and DEV is None: + self.console.toggle_view_action.setChecked(False) + self.console.dockwidget.hide() + + # Show Help and Consoles by default + plugins_to_show = [] + if self.help is not None: + plugins_to_show.append(self.help) + if self.ipyconsole is not None: + if self.ipyconsole.isvisible: + plugins_to_show += [self.extconsole, self.ipyconsole] + else: + plugins_to_show += [self.ipyconsole, self.extconsole] + else: + plugins_to_show += [self.extconsole] + for plugin in plugins_to_show: + if plugin.dockwidget.isVisible(): + plugin.dockwidget.raise_() + + # Show history file if no console is visible + ipy_visible = self.ipyconsole is not None and self.ipyconsole.isvisible + if not self.extconsole.isvisible and not ipy_visible: + self.historylog.add_history(get_conf_path('history.py')) + + # Load last project if a project was active when Spyder + # was closed + self.projects.reopen_last_project() + + # If no project is active, load last session + if self.projects.get_active_project() is None: + self.editor.setup_open_files() + + # Check for spyder updates + if DEV is None and CONF.get('main', 'check_updates_on_startup'): + self.give_updates_feedback = False + self.check_updates() + + # Show dialog with missing dependencies + self.report_missing_dependencies() + + self.is_setting_up = False + + def update_window_title(self): + """Update main spyder window title based on projects.""" + title = self.base_title + if self.projects is not None: + path = self.projects.get_active_project_path() + if path: + path = path.replace(get_home_dir(), '~') + title = '{0} - {1}'.format(path, title) + self.setWindowTitle(title) + + def report_missing_dependencies(self): + """Show a QMessageBox with a list of missing hard dependencies""" + missing_deps = dependencies.missing_dependencies() + if missing_deps: + QMessageBox.critical(self, _('Error'), + _("You have missing dependencies!" + "

%s

" + "Please install them to avoid this message." + "

" + "Note: Spyder could work without some of these " + "dependencies, however to have a smooth experience when " + "using Spyder we strongly recommend you to install " + "all the listed missing dependencies.

" + "Failing to install these dependencies might result in bugs. " + "Please be sure that any found bugs are not the direct " + "result of missing dependencies, prior to reporting a new " + "issue." + ) % missing_deps, QMessageBox.Ok) + + def load_window_settings(self, prefix, default=False, section='main'): + """Load window layout settings from userconfig-based configuration + with *prefix*, under *section* + default: if True, do not restore inner layout""" + get_func = CONF.get_default if default else CONF.get + window_size = get_func(section, prefix+'size') + prefs_dialog_size = get_func(section, prefix+'prefs_dialog_size') + if default: + hexstate = None + else: + hexstate = get_func(section, prefix+'state', None) + pos = get_func(section, prefix+'position') + is_maximized = get_func(section, prefix+'is_maximized') + is_fullscreen = get_func(section, prefix+'is_fullscreen') + return hexstate, window_size, prefs_dialog_size, pos, is_maximized, \ + is_fullscreen + + def get_window_settings(self): + """Return current window settings + Symetric to the 'set_window_settings' setter""" + window_size = (self.window_size.width(), self.window_size.height()) + is_fullscreen = self.isFullScreen() + if is_fullscreen: + is_maximized = self.maximized_flag + else: + is_maximized = self.isMaximized() + pos = (self.window_position.x(), self.window_position.y()) + prefs_dialog_size = (self.prefs_dialog_size.width(), + self.prefs_dialog_size.height()) + hexstate = qbytearray_to_str(self.saveState()) + return (hexstate, window_size, prefs_dialog_size, pos, is_maximized, + is_fullscreen) + + def set_window_settings(self, hexstate, window_size, prefs_dialog_size, + pos, is_maximized, is_fullscreen): + """Set window settings + Symetric to the 'get_window_settings' accessor""" + self.setUpdatesEnabled(False) + self.window_size = QSize(window_size[0], window_size[1]) # width,height + self.prefs_dialog_size = QSize(prefs_dialog_size[0], + prefs_dialog_size[1]) # width,height + self.window_position = QPoint(pos[0], pos[1]) # x,y + self.setWindowState(Qt.WindowNoState) + self.resize(self.window_size) + self.move(self.window_position) + + # Window layout + if hexstate: + self.restoreState( QByteArray().fromHex( + str(hexstate).encode('utf-8')) ) + # [Workaround for Issue 880] + # QDockWidget objects are not painted if restored as floating + # windows, so we must dock them before showing the mainwindow. + for widget in self.children(): + if isinstance(widget, QDockWidget) and widget.isFloating(): + self.floating_dockwidgets.append(widget) + widget.setFloating(False) + + # Is fullscreen? + if is_fullscreen: + self.setWindowState(Qt.WindowFullScreen) + self.__update_fullscreen_action() + + # Is maximized? + if is_fullscreen: + self.maximized_flag = is_maximized + elif is_maximized: + self.setWindowState(Qt.WindowMaximized) + self.setUpdatesEnabled(True) + + def save_current_window_settings(self, prefix, section='main'): + """Save current window settings with *prefix* in + the userconfig-based configuration, under *section*""" + win_size = self.window_size + prefs_size = self.prefs_dialog_size + + CONF.set(section, prefix+'size', (win_size.width(), win_size.height())) + CONF.set(section, prefix+'prefs_dialog_size', + (prefs_size.width(), prefs_size.height())) + CONF.set(section, prefix+'is_maximized', self.isMaximized()) + CONF.set(section, prefix+'is_fullscreen', self.isFullScreen()) + pos = self.window_position + CONF.set(section, prefix+'position', (pos.x(), pos.y())) + self.maximize_dockwidget(restore=True)# Restore non-maximized layout + qba = self.saveState() + CONF.set(section, prefix+'state', qbytearray_to_str(qba)) + CONF.set(section, prefix+'statusbar', + not self.statusBar().isHidden()) + + def tabify_plugins(self, first, second): + """Tabify plugin dockwigdets""" + self.tabifyDockWidget(first.dockwidget, second.dockwidget) + + # --- Layouts + def setup_layout(self, default=False): + """Setup window layout""" + prefix = 'window' + '/' + settings = self.load_window_settings(prefix, default) + hexstate = settings[0] + + self.first_spyder_run = False + if hexstate is None: + # First Spyder execution: + self.setWindowState(Qt.WindowMaximized) + self.first_spyder_run = True + self.setup_default_layouts('default', settings) + self.extconsole.setMinimumHeight(250) + + # Now that the initial setup is done, copy the window settings, + # except for the hexstate in the quick layouts sections for the + # default layouts. + # Order and name of the default layouts is found in config.py + section = 'quick_layouts' + get_func = CONF.get_default if default else CONF.get + order = get_func(section, 'order') + + # restore the original defaults if reset layouts is called + if default: + CONF.set(section, 'active', order) + CONF.set(section, 'order', order) + CONF.set(section, 'names', order) + + for index, name, in enumerate(order): + prefix = 'layout_{0}/'.format(index) + self.save_current_window_settings(prefix, section) + CONF.set(section, prefix+'state', None) + + # store the initial layout as the default in spyder + prefix = 'layout_default/' + section = 'quick_layouts' + self.save_current_window_settings(prefix, section) + self.current_quick_layout = 'default' + CONF.set(section, prefix+'state', None) + + # Regenerate menu + self.quick_layout_set_menu() + self.set_window_settings(*settings) + + for plugin in self.widgetlist: + try: + plugin.initialize_plugin_in_mainwindow_layout() + except Exception as error: + print("%s: %s" % (plugin, str(error)), file=STDERR) + traceback.print_exc(file=STDERR) + + def setup_default_layouts(self, index, settings): + """Setup default layouts when run for the first time""" + self.set_window_settings(*settings) + self.setUpdatesEnabled(False) + + # IMPORTANT: order has to be the same as defined in the config file + MATLAB, RSTUDIO, VERTICAL, HORIZONTAL = range(4) + + # define widgets locally + editor = self.editor + console_ipy = self.ipyconsole + console_ext = self.extconsole + console_int = self.console + outline = self.outlineexplorer + explorer_project = self.projects + explorer_file = self.explorer + explorer_variable = self.variableexplorer + history = self.historylog + finder = self.findinfiles + help_plugin = self.help + helper = self.onlinehelp + plugins = self.thirdparty_plugins + + global_hidden_widgets = [finder, console_int, explorer_project, + helper] + plugins + global_hidden_toolbars = [self.source_toolbar, self.edit_toolbar, + self.search_toolbar] + # Layout definition + # layouts are organized by columns, each colum is organized by rows + # widths have to add 1.0, height per column have to add 1.0 + # Spyder Default Initial Layout + s_layout = {'widgets': [ + # column 0 + [[explorer_project]], + # column 1 + [[editor]], + # column 2 + [[outline]], + # column 3 + [[help_plugin, explorer_variable, helper, explorer_file, + finder] + plugins, + [console_int, console_ext, console_ipy, history]] + ], + 'width fraction': [0.0, # column 0 width + 0.55, # column 1 width + 0.0, # column 2 width + 0.45], # column 3 width + 'height fraction': [[1.0], # column 0, row heights + [1.0], # column 1, row heights + [1.0], # column 2, row heights + [0.46, 0.54]], # column 3, row heights + 'hidden widgets': [outline], + 'hidden toolbars': [], + } + r_layout = {'widgets': [ + # column 0 + [[editor], + [console_ipy, console_ext, console_int]], + # column 1 + [[explorer_variable, history, outline, finder] + plugins, + [explorer_file, explorer_project, help_plugin, helper]] + ], + 'width fraction': [0.55, # column 0 width + 0.45], # column 1 width + 'height fraction': [[0.55, 0.45], # column 0, row heights + [0.55, 0.45]], # column 1, row heights + 'hidden widgets': [outline], + 'hidden toolbars': [], + } + # Matlab + m_layout = {'widgets': [ + # column 0 + [[explorer_file, explorer_project], + [outline]], + # column 1 + [[editor], + [console_ipy, console_ext, console_int]], + # column 2 + [[explorer_variable, finder] + plugins, + [history, help_plugin, helper]] + ], + 'width fraction': [0.20, # column 0 width + 0.40, # column 1 width + 0.40], # column 2 width + 'height fraction': [[0.55, 0.45], # column 0, row heights + [0.55, 0.45], # column 1, row heights + [0.55, 0.45]], # column 2, row heights + 'hidden widgets': [], + 'hidden toolbars': [], + } + # Vertically split + v_layout = {'widgets': [ + # column 0 + [[editor], + [console_ipy, console_ext, console_int, explorer_file, + explorer_project, help_plugin, explorer_variable, + history, outline, finder, helper] + plugins] + ], + 'width fraction': [1.0], # column 0 width + 'height fraction': [[0.55, 0.45]], # column 0, row heights + 'hidden widgets': [outline], + 'hidden toolbars': [], + } + # Horizontally split + h_layout = {'widgets': [ + # column 0 + [[editor]], + # column 1 + [[console_ipy, console_ext, console_int, explorer_file, + explorer_project, help_plugin, explorer_variable, + history, outline, finder, helper] + plugins] + ], + 'width fraction': [0.55, # column 0 width + 0.45], # column 1 width + 'height fraction': [[1.0], # column 0, row heights + [1.0]], # column 1, row heights + 'hidden widgets': [outline], + 'hidden toolbars': [] + } + + # Layout selection + layouts = {'default': s_layout, + RSTUDIO: r_layout, + MATLAB: m_layout, + VERTICAL: v_layout, + HORIZONTAL: h_layout} + + layout = layouts[index] + + widgets_layout = layout['widgets'] + widgets = [] + for column in widgets_layout : + for row in column: + for widget in row: + if widget is not None: + widgets.append(widget) + + # Make every widget visible + for widget in widgets: + widget.toggle_view(True) + action = widget.toggle_view_action + action.setChecked(widget.dockwidget.isVisible()) + + # Set the widgets horizontally + for i in range(len(widgets) - 1): + first, second = widgets[i], widgets[i+1] + if first is not None and second is not None: + self.splitDockWidget(first.dockwidget, second.dockwidget, + Qt.Horizontal) + + # Arrange rows vertically + for column in widgets_layout : + for i in range(len(column) - 1): + first_row, second_row = column[i], column[i+1] + if first_row is not None and second_row is not None: + self.splitDockWidget(first_row[0].dockwidget, + second_row[0].dockwidget, + Qt.Vertical) + # Tabify + for column in widgets_layout : + for row in column: + for i in range(len(row) - 1): + first, second = row[i], row[i+1] + if first is not None and second is not None: + self.tabify_plugins(first, second) + + # Raise front widget per row + row[0].dockwidget.show() + row[0].dockwidget.raise_() + + # Hide toolbars + hidden_toolbars = global_hidden_toolbars + layout['hidden toolbars'] + for toolbar in hidden_toolbars: + if toolbar is not None: + toolbar.close() + + # Hide widgets + hidden_widgets = global_hidden_widgets + layout['hidden widgets'] + for widget in hidden_widgets: + if widget is not None: + widget.dockwidget.close() + + # set the width and height + self._layout_widget_info = [] + width, height = self.window_size.width(), self.window_size.height() + + # fix column width +# for c in range(len(widgets_layout)): +# widget = widgets_layout[c][0][0].dockwidget +# min_width, max_width = widget.minimumWidth(), widget.maximumWidth() +# info = {'widget': widget, +# 'min width': min_width, +# 'max width': max_width} +# self._layout_widget_info.append(info) +# new_width = int(layout['width fraction'][c] * width * 0.95) +# widget.setMinimumWidth(new_width) +# widget.setMaximumWidth(new_width) +# widget.updateGeometry() +# print(c, widgets_layout[c][0][0], new_width) + + # fix column height + for c, column in enumerate(widgets_layout): + for r in range(len(column) - 1): + widget = column[r][0] + dockwidget = widget.dockwidget + dock_min_h = dockwidget.minimumHeight() + dock_max_h = dockwidget.maximumHeight() + info = {'widget': widget, + 'dock min height': dock_min_h, + 'dock max height': dock_max_h} + self._layout_widget_info.append(info) + # The 0.95 factor is to adjust height based on usefull + # estimated area in the window + new_height = int(layout['height fraction'][c][r]*height*0.95) + dockwidget.setMinimumHeight(new_height) + dockwidget.setMaximumHeight(new_height) + + self._custom_layout_timer = QTimer(self) + self._custom_layout_timer.timeout.connect(self.layout_fix_timer) + self._custom_layout_timer.setSingleShot(True) + self._custom_layout_timer.start(5000) + + def layout_fix_timer(self): + """Fixes the height of docks after a new layout is set.""" + info = self._layout_widget_info + for i in info: + dockwidget = i['widget'].dockwidget + if 'dock min width' in i: + dockwidget.setMinimumWidth(i['dock min width']) + dockwidget.setMaximumWidth(i['dock max width']) + if 'dock min height' in i: + dockwidget.setMinimumHeight(i['dock min height']) + dockwidget.setMaximumHeight(i['dock max height']) + dockwidget.updateGeometry() + + self.setUpdatesEnabled(True) + + @Slot() + def toggle_previous_layout(self): + """ """ + self.toggle_layout('previous') + + @Slot() + def toggle_next_layout(self): + """ """ + self.toggle_layout('next') + + def toggle_layout(self, direction='next'): + """ """ + get = CONF.get + names = get('quick_layouts', 'names') + order = get('quick_layouts', 'order') + active = get('quick_layouts', 'active') + + if len(active) == 0: + return + + layout_index = ['default'] + for name in order: + if name in active: + layout_index.append(names.index(name)) + + current_layout = self.current_quick_layout + dic = {'next': 1, 'previous': -1} + + if current_layout is None: + # Start from default + current_layout = 'default' + + if current_layout in layout_index: + current_index = layout_index.index(current_layout) + else: + current_index = 0 + + new_index = (current_index + dic[direction]) % len(layout_index) + self.quick_layout_switch(layout_index[new_index]) + + def quick_layout_set_menu(self): + """ """ + get = CONF.get + names = get('quick_layouts', 'names') + order = get('quick_layouts', 'order') + active = get('quick_layouts', 'active') + + ql_actions = [] + + ql_actions = [create_action(self, _('Spyder Default Layout'), + triggered=lambda: + self.quick_layout_switch('default'))] + for name in order: + if name in active: + index = names.index(name) + + # closure required so lambda works with the default parameter + def trigger(i=index, self=self): + return lambda: self.quick_layout_switch(i) + + qli_act = create_action(self, name, triggered=trigger()) + # closure above replaces the following which stopped working + # qli_act = create_action(self, name, triggered=lambda i=index: + # self.quick_layout_switch(i) + + ql_actions += [qli_act] + + self.ql_save = create_action(self, _("Save current layout"), + triggered=lambda: + self.quick_layout_save(), + context=Qt.ApplicationShortcut) + self.ql_preferences = create_action(self, _("Layout preferences"), + triggered=lambda: + self.quick_layout_settings(), + context=Qt.ApplicationShortcut) + self.ql_reset = create_action(self, _('Reset to spyder default'), + triggered=self.reset_window_layout) + + self.register_shortcut(self.ql_save, "_", "Save current layout") + self.register_shortcut(self.ql_preferences, "_", "Layout preferences") + + ql_actions += [None] + ql_actions += [self.ql_save, self.ql_preferences, self.ql_reset] + + self.quick_layout_menu.clear() + add_actions(self.quick_layout_menu, ql_actions) + + if len(order) == 0: + self.ql_preferences.setEnabled(False) + else: + self.ql_preferences.setEnabled(True) + + @Slot() + def reset_window_layout(self): + """Reset window layout to default""" + answer = QMessageBox.warning(self, _("Warning"), + _("Window layout will be reset to default settings: " + "this affects window position, size and dockwidgets.\n" + "Do you want to continue?"), + QMessageBox.Yes | QMessageBox.No) + if answer == QMessageBox.Yes: + self.setup_layout(default=True) + + def quick_layout_save(self): + """Save layout dialog""" + get = CONF.get + set_ = CONF.set + names = get('quick_layouts', 'names') + order = get('quick_layouts', 'order') + active = get('quick_layouts', 'active') + + dlg = self.dialog_layout_save(self, names) + + if dlg.exec_(): + name = dlg.combo_box.currentText() + + if name in names: + answer = QMessageBox.warning(self, _("Warning"), + _("Layout %s will be \ + overwritten. Do you want to \ + continue?") % name, + QMessageBox.Yes | QMessageBox.No) + index = order.index(name) + else: + answer = True + if None in names: + index = names.index(None) + names[index] = name + else: + index = len(names) + names.append(name) + order.append(name) + + # Always make active a new layout even if it overwrites an inactive + # layout + if name not in active: + active.append(name) + + if answer: + self.save_current_window_settings('layout_{}/'.format(index), + section='quick_layouts') + set_('quick_layouts', 'names', names) + set_('quick_layouts', 'order', order) + set_('quick_layouts', 'active', active) + self.quick_layout_set_menu() + + def quick_layout_settings(self): + """Layout settings dialog""" + get = CONF.get + set_ = CONF.set + + section = 'quick_layouts' + + names = get(section, 'names') + order = get(section, 'order') + active = get(section, 'active') + + dlg = self.dialog_layout_settings(self, names, order, active) + if dlg.exec_(): + set_(section, 'names', dlg.names) + set_(section, 'order', dlg.order) + set_(section, 'active', dlg.active) + self.quick_layout_set_menu() + + def quick_layout_switch(self, index): + """Switch to quick layout number *index*""" + section = 'quick_layouts' + + try: + settings = self.load_window_settings('layout_{}/'.format(index), + section=section) + (hexstate, window_size, prefs_dialog_size, pos, is_maximized, + is_fullscreen) = settings + + # The defaults layouts will alwyas be regenerated unless there was + # an overwrite, either by rewriting with same name, or by deleting + # and then creating a new one + if hexstate is None: + self.setup_default_layouts(index, settings) + except cp.NoOptionError: + QMessageBox.critical(self, _("Warning"), + _("Quick switch layout #%s has not yet " + "been defined.") % str(index)) + return + # TODO: is there any real use in calling the previous layout + # setting? + # self.previous_layout_settings = self.get_window_settings() + self.set_window_settings(*settings) + self.current_quick_layout = index + + # make sure the flags are correctly set for visible panes + for plugin in self.widgetlist: + action = plugin.toggle_view_action + action.setChecked(plugin.dockwidget.isVisible()) + + # --- Show/Hide toolbars + def _update_show_toolbars_action(self): + """Update the text displayed in the menu entry.""" + if self.toolbars_visible: + text = _("Hide toolbars") + tip = _("Hide toolbars") + else: + text = _("Show toolbars") + tip = _("Show toolbars") + self.show_toolbars_action.setText(text) + self.show_toolbars_action.setToolTip(tip) + + def save_visible_toolbars(self): + """Saves the name of the visible toolbars in the .ini file.""" + toolbars = [] + for toolbar in self.visible_toolbars: + toolbars.append(toolbar.objectName()) + CONF.set('main', 'last_visible_toolbars', toolbars) + + def get_visible_toolbars(self): + """Collects the visible toolbars.""" + toolbars = [] + for toolbar in self.toolbarslist: + if toolbar.toggleViewAction().isChecked(): + toolbars.append(toolbar) + self.visible_toolbars = toolbars + + def load_last_visible_toolbars(self): + """Loads the last visible toolbars from the .ini file.""" + toolbars_names = CONF.get('main', 'last_visible_toolbars', default=[]) + + if toolbars_names: + dic = {} + for toolbar in self.toolbarslist: + dic[toolbar.objectName()] = toolbar + + toolbars = [] + for name in toolbars_names: + if name in dic: + toolbars.append(dic[name]) + self.visible_toolbars = toolbars + else: + self.get_visible_toolbars() + self._update_show_toolbars_action() + + @Slot() + def show_toolbars(self): + """Show/Hides toolbars.""" + value = not self.toolbars_visible + CONF.set('main', 'toolbars_visible', value) + if value: + self.save_visible_toolbars() + else: + self.get_visible_toolbars() + + for toolbar in self.visible_toolbars: + toolbar.toggleViewAction().setChecked(value) + toolbar.setVisible(value) + + self.toolbars_visible = value + self._update_show_toolbars_action() + + # --- Other + def plugin_focus_changed(self): + """Focus has changed from one plugin to another""" + self.update_edit_menu() + self.update_search_menu() + + # Now deal with Python shell and IPython plugins + if self.ipyconsole is not None: + focus_client = self.ipyconsole.get_focus_client() + if focus_client is not None: + self.last_console_plugin_focus_was_python = False + else: + shell = get_focus_python_shell() + if shell is not None: + self.last_console_plugin_focus_was_python = True + + def show_shortcuts(self, menu): + """Show action shortcuts in menu""" + for element in getattr(self, menu + '_menu_actions'): + if element and isinstance(element, QAction): + if element._shown_shortcut is not None: + element.setShortcut(element._shown_shortcut) + + def hide_shortcuts(self, menu): + """Hide action shortcuts in menu""" + for element in getattr(self, menu + '_menu_actions'): + if element and isinstance(element, QAction): + if element._shown_shortcut is not None: + element.setShortcut(QKeySequence()) + + def update_edit_menu(self): + """Update edit menu""" + if self.menuBar().hasFocus(): + return + # Disabling all actions to begin with + for child in self.edit_menu.actions(): + child.setEnabled(False) + + widget, textedit_properties = get_focus_widget_properties() + if textedit_properties is None: # widget is not an editor/console + return + #!!! Below this line, widget is expected to be a QPlainTextEdit instance + console, not_readonly, readwrite_editor = textedit_properties + + # Editor has focus and there is no file opened in it + if not console and not_readonly and not self.editor.is_file_opened(): + return + + self.selectall_action.setEnabled(True) + + # Undo, redo + self.undo_action.setEnabled( readwrite_editor \ + and widget.document().isUndoAvailable() ) + self.redo_action.setEnabled( readwrite_editor \ + and widget.document().isRedoAvailable() ) + + # Copy, cut, paste, delete + has_selection = widget.has_selected_text() + self.copy_action.setEnabled(has_selection) + self.cut_action.setEnabled(has_selection and not_readonly) + self.paste_action.setEnabled(not_readonly) + + # Comment, uncomment, indent, unindent... + if not console and not_readonly: + # This is the editor and current file is writable + for action in self.editor.edit_menu_actions: + action.setEnabled(True) + + def update_search_menu(self): + """Update search menu""" + if self.menuBar().hasFocus(): + return + + widget, textedit_properties = get_focus_widget_properties() + for action in self.editor.search_menu_actions: + action.setEnabled(self.editor.isAncestorOf(widget)) + if textedit_properties is None: # widget is not an editor/console + return + #!!! Below this line, widget is expected to be a QPlainTextEdit instance + _x, _y, readwrite_editor = textedit_properties + # Disable the replace action for read-only files + self.search_menu_actions[3].setEnabled(readwrite_editor) + + def create_plugins_menu(self): + order = ['editor', 'console', 'ipython_console', 'variable_explorer', + 'help', None, 'explorer', 'outline_explorer', + 'project_explorer', 'find_in_files', None, 'historylog', + 'profiler', 'breakpoints', 'pylint', None, + 'onlinehelp', 'internal_console'] + for plugin in self.widgetlist: + action = plugin.toggle_view_action + action.setChecked(plugin.dockwidget.isVisible()) + try: + name = plugin.CONF_SECTION + pos = order.index(name) + except ValueError: + pos = None + if pos is not None: + order[pos] = action + else: + order.append(action) + actions = order[:] + for action in order: + if type(action) is str: + actions.remove(action) + self.plugins_menu_actions = actions + add_actions(self.plugins_menu, actions) + + def create_toolbars_menu(self): + order = ['file_toolbar', 'run_toolbar', 'debug_toolbar', + 'main_toolbar', 'Global working directory', None, + 'search_toolbar', 'edit_toolbar', 'source_toolbar'] + for toolbar in self.toolbarslist: + action = toolbar.toggleViewAction() + name = toolbar.objectName() + try: + pos = order.index(name) + except ValueError: + pos = None + if pos is not None: + order[pos] = action + else: + order.append(action) + add_actions(self.toolbars_menu, order) + + def createPopupMenu(self): + menu = QMenu('', self) + actions = self.help_menu_actions[:3] + \ + [None, self.help_menu_actions[-1]] + add_actions(menu, actions) + return menu + + def set_splash(self, message): + """Set splash message""" + if message: + self.debug_print(message) + self.splash.show() + self.splash.showMessage(message, Qt.AlignBottom | Qt.AlignCenter | + Qt.AlignAbsolute, QColor(Qt.white)) + QApplication.processEvents() + + def remove_tmpdir(self): + """Remove Spyder temporary directory""" + shutil.rmtree(programs.TEMPDIR, ignore_errors=True) + + def closeEvent(self, event): + """closeEvent reimplementation""" + if self.closing(True): + event.accept() + else: + event.ignore() + + def resizeEvent(self, event): + """Reimplement Qt method""" + if not self.isMaximized() and not self.fullscreen_flag: + self.window_size = self.size() + QMainWindow.resizeEvent(self, event) + + # To be used by the tour to be able to resize + self.sig_resized.emit(event) + + def moveEvent(self, event): + """Reimplement Qt method""" + if not self.isMaximized() and not self.fullscreen_flag: + self.window_position = self.pos() + QMainWindow.moveEvent(self, event) + + # To be used by the tour to be able to move + self.sig_moved.emit(event) + + def hideEvent(self, event): + """Reimplement Qt method""" + for plugin in self.widgetlist: + if plugin.isAncestorOf(self.last_focused_widget): + plugin.visibility_changed(True) + QMainWindow.hideEvent(self, event) + + def change_last_focused_widget(self, old, now): + """To keep track of to the last focused widget""" + if (now is None and QApplication.activeWindow() is not None): + QApplication.activeWindow().setFocus() + self.last_focused_widget = QApplication.focusWidget() + elif now is not None: + self.last_focused_widget = now + + def closing(self, cancelable=False): + """Exit tasks""" + if self.already_closed or self.is_starting_up: + return True + if cancelable and CONF.get('main', 'prompt_on_exit'): + reply = QMessageBox.critical(self, 'Spyder', + 'Do you really want to exit?', + QMessageBox.Yes, QMessageBox.No) + if reply == QMessageBox.No: + return False + prefix = 'window' + '/' + self.save_current_window_settings(prefix) + if CONF.get('main', 'single_instance'): + self.open_files_server.close() + for plugin in self.thirdparty_plugins: + if not plugin.closing_plugin(cancelable): + return False + for widget in self.widgetlist: + if not widget.closing_plugin(cancelable): + return False + self.dialog_manager.close_all() + if self.toolbars_visible: + self.save_visible_toolbars() + self.already_closed = True + return True + + def add_dockwidget(self, child): + """Add QDockWidget and toggleViewAction""" + dockwidget, location = child.create_dockwidget() + if CONF.get('main', 'vertical_dockwidget_titlebars'): + dockwidget.setFeatures(dockwidget.features()| + QDockWidget.DockWidgetVerticalTitleBar) + self.addDockWidget(location, dockwidget) + self.widgetlist.append(child) + + @Slot() + def close_current_dockwidget(self): + widget = QApplication.focusWidget() + for plugin in self.widgetlist: + if plugin.isAncestorOf(widget): + plugin.dockwidget.hide() + break + + def toggle_lock_dockwidgets(self, value): + """Lock/Unlock dockwidgets""" + self.dockwidgets_locked = value + self.apply_panes_settings() + CONF.set('main', 'panes_locked', value) + + def __update_maximize_action(self): + if self.state_before_maximizing is None: + text = _("Maximize current pane") + tip = _("Maximize current pane") + icon = ima.icon('maximize') + else: + text = _("Restore current pane") + tip = _("Restore pane to its original size") + icon = ima.icon('unmaximize') + self.maximize_action.setText(text) + self.maximize_action.setIcon(icon) + self.maximize_action.setToolTip(tip) + + @Slot() + def maximize_dockwidget(self, restore=False): + """Shortcut: Ctrl+Alt+Shift+M + First call: maximize current dockwidget + Second call (or restore=True): restore original window layout""" + if self.state_before_maximizing is None: + if restore: + return + # No plugin is currently maximized: maximizing focus plugin + self.state_before_maximizing = self.saveState() + focus_widget = QApplication.focusWidget() + for plugin in self.widgetlist: + plugin.dockwidget.hide() + if plugin.isAncestorOf(focus_widget): + self.last_plugin = plugin + self.last_plugin.dockwidget.toggleViewAction().setDisabled(True) + self.setCentralWidget(self.last_plugin) + self.last_plugin.ismaximized = True + # Workaround to solve an issue with editor's outline explorer: + # (otherwise the whole plugin is hidden and so is the outline explorer + # and the latter won't be refreshed if not visible) + self.last_plugin.show() + self.last_plugin.visibility_changed(True) + if self.last_plugin is self.editor: + # Automatically show the outline if the editor was maximized: + self.addDockWidget(Qt.RightDockWidgetArea, + self.outlineexplorer.dockwidget) + self.outlineexplorer.dockwidget.show() + else: + # Restore original layout (before maximizing current dockwidget) + self.last_plugin.dockwidget.setWidget(self.last_plugin) + self.last_plugin.dockwidget.toggleViewAction().setEnabled(True) + self.setCentralWidget(None) + self.last_plugin.ismaximized = False + self.restoreState(self.state_before_maximizing) + self.state_before_maximizing = None + self.last_plugin.get_focus_widget().setFocus() + self.__update_maximize_action() + + def __update_fullscreen_action(self): + if self.isFullScreen(): + icon = ima.icon('window_nofullscreen') + else: + icon = ima.icon('window_fullscreen') + if is_text_string(icon): + icon = get_icon(icon) + self.fullscreen_action.setIcon(icon) + + @Slot() + def toggle_fullscreen(self): + if self.isFullScreen(): + self.fullscreen_flag = False + self.showNormal() + if self.maximized_flag: + self.showMaximized() + else: + self.maximized_flag = self.isMaximized() + self.fullscreen_flag = True + self.showFullScreen() + self.__update_fullscreen_action() + + def add_to_toolbar(self, toolbar, widget): + """Add widget actions to toolbar""" + actions = widget.toolbar_actions + if actions is not None: + add_actions(toolbar, actions) + + @Slot() + def about(self): + """About Spyder""" + versions = get_versions() + # Show Mercurial revision for development version + revlink = '' + if versions['revision']: + rev = versions['revision'] + revlink = " (Commit: %s)" % (rev, rev) + QMessageBox.about(self, + _("About %s") % "Spyder", + """Spyder %s %s +
The Scientific PYthon Development EnviRonment +
Copyright © The Spyder Project Contributors +
Licensed under the terms of the MIT License +

Created by Pierre Raybaut. +
Developed and maintained by the + Spyder Project Contributors. +
Many thanks to all the Spyder beta testers and regular users. +

For bug reports and feature requests, please go + to our Github website. For discussions around the + project, please go to our Google Group +

This project is part of a larger effort to promote and + facilitate the use of Python for scientific and engineering + software development. The popular Python distributions + Anaconda, + WinPython and + Python(x,y) + also contribute to this plan. +

Python %s %dbits, Qt %s, %s %s on %s +

Most of the icons for the Spyder 2 theme come from the Crystal + Project (© 2006-2007 Everaldo Coelho). Other icons for that + theme come from Yusuke + Kamiyamane (all rights reserved) and from + + The Oxygen icon theme. + """ + % (versions['spyder'], revlink, __project_url__, + __project_url__, __forum_url__, versions['python'], + versions['bitness'], versions['qt'], versions['qt_api'], + versions['qt_api_ver'], versions['system'])) + + @Slot() + def show_dependencies(self): + """Show Spyder's Dependencies dialog box""" + from spyder.widgets.dependencies import DependenciesDialog + dlg = DependenciesDialog(None) + dlg.set_data(dependencies.DEPENDENCIES) + dlg.show() + dlg.exec_() + + @Slot() + def report_issue(self): + if PY3: + from urllib.parse import quote + else: + from urllib import quote # analysis:ignore + versions = get_versions() + # Get git revision for development version + revision = '' + if versions['revision']: + revision = versions['revision'] + issue_template = """\ +## Description + +**What steps will reproduce the problem?** + +1. +2. +3. + +**What is the expected output? What do you see instead?** + + +**Please provide any additional information below** + + +## Version and main components + +* Spyder Version: %s %s +* Python Version: %s +* Qt Versions: %s, %s %s on %s + +## Dependencies +``` +%s +``` +""" % (versions['spyder'], + revision, + versions['python'], + versions['qt'], + versions['qt_api'], + versions['qt_api_ver'], + versions['system'], + dependencies.status()) + + url = QUrl("https://github.com/spyder-ide/spyder/issues/new") + if PYQT5: + from qtpy.QtCore import QUrlQuery + query = QUrlQuery() + query.addQueryItem("body", quote(issue_template)) + url.setQuery(query) + else: + url.addEncodedQueryItem("body", quote(issue_template)) + + QDesktopServices.openUrl(url) + + @Slot() + def google_group(self): + url = QUrl("http://groups.google.com/group/spyderlib") + QDesktopServices.openUrl(url) + + @Slot() + def global_callback(self): + """Global callback""" + widget = QApplication.focusWidget() + action = self.sender() + callback = from_qvariant(action.data(), to_text_string) + from spyder.widgets.editor import TextEditBaseWidget + if isinstance(widget, TextEditBaseWidget): + getattr(widget, callback)() + + def redirect_internalshell_stdio(self, state): + if state: + self.console.shell.interpreter.redirect_stds() + else: + self.console.shell.interpreter.restore_stds() + + def open_external_console(self, fname, wdir, args, interact, debug, python, + python_args, systerm, post_mortem=False): + """Open external console""" + if systerm: + # Running script in an external system terminal + try: + programs.run_python_script_in_terminal(fname, wdir, args, + interact, debug, python_args) + except NotImplementedError: + QMessageBox.critical(self, _("Run"), + _("Running an external system terminal " + "is not supported on platform %s." + ) % os.name) + else: + self.extconsole.visibility_changed(True) + self.extconsole.raise_() + self.extconsole.start( + fname=to_text_string(fname), wdir=to_text_string(wdir), + args=to_text_string(args), interact=interact, + debug=debug, python=python, post_mortem=post_mortem, + python_args=to_text_string(python_args) ) + + def execute_in_external_console(self, lines, focus_to_editor): + """ + Execute lines in external or IPython console and eventually set focus + to the editor + """ + console = self.extconsole + if self.ipyconsole is None or self.last_console_plugin_focus_was_python: + console = self.extconsole + else: + console = self.ipyconsole + console.visibility_changed(True) + console.raise_() + console.execute_code(lines) + if focus_to_editor: + self.editor.visibility_changed(True) + + def open_file(self, fname, external=False): + """ + Open filename with the appropriate application + Redirect to the right widget (txt -> editor, spydata -> workspace, ...) + or open file outside Spyder (if extension is not supported) + """ + fname = to_text_string(fname) + ext = osp.splitext(fname)[1] + if encoding.is_text_file(fname): + self.editor.load(fname) + elif self.variableexplorer is not None and ext in IMPORT_EXT: + self.variableexplorer.import_data(fname) + elif not external: + fname = file_uri(fname) + programs.start_file(fname) + + def open_external_file(self, fname): + """ + Open external files that can be handled either by the Editor or the + variable explorer inside Spyder. + """ + fname = encoding.to_unicode_from_fs(fname) + if osp.isfile(fname): + self.open_file(fname, external=True) + elif osp.isfile(osp.join(CWD, fname)): + self.open_file(osp.join(CWD, fname), external=True) + + #---- PYTHONPATH management, etc. + def get_spyder_pythonpath(self): + """Return Spyder PYTHONPATH""" + return self.path+self.project_path + + def add_path_to_sys_path(self): + """Add Spyder path to sys.path""" + for path in reversed(self.get_spyder_pythonpath()): + sys.path.insert(1, path) + + def remove_path_from_sys_path(self): + """Remove Spyder path from sys.path""" + sys_path = sys.path + while sys_path[1] in self.get_spyder_pythonpath(): + sys_path.pop(1) + + @Slot() + def path_manager_callback(self): + """Spyder path manager""" + from spyder.widgets.pathmanager import PathManager + self.remove_path_from_sys_path() + project_path = self.projects.get_pythonpath() + dialog = PathManager(self, self.path, project_path, sync=True) + dialog.redirect_stdio.connect(self.redirect_internalshell_stdio) + dialog.exec_() + self.add_path_to_sys_path() + encoding.writelines(self.path, self.SPYDER_PATH) # Saving path + self.sig_pythonpath_changed.emit() + + def pythonpath_changed(self): + """Projects PYTHONPATH contribution has changed""" + self.remove_path_from_sys_path() + self.project_path = self.projects.get_pythonpath() + self.add_path_to_sys_path() + self.sig_pythonpath_changed.emit() + + @Slot() + def win_env(self): + """Show Windows current user environment variables""" + self.dialog_manager.show(WinUserEnvDialog(self)) + + #---- Preferences + def apply_settings(self): + """Apply settings changed in 'Preferences' dialog box""" + qapp = QApplication.instance() + # Set 'gtk+' as the default theme in Gtk-based desktops + # Fixes Issue 2036 + if is_gtk_desktop() and ('GTK+' in QStyleFactory.keys()): + try: + qapp.setStyle('gtk+') + except: + pass + else: + qapp.setStyle(CONF.get('main', 'windows_style', + self.default_style)) + + default = self.DOCKOPTIONS + if CONF.get('main', 'vertical_tabs'): + default = default|QMainWindow.VerticalTabs + if CONF.get('main', 'animated_docks'): + default = default|QMainWindow.AnimatedDocks + self.setDockOptions(default) + + self.apply_panes_settings() + self.apply_statusbar_settings() + + def apply_panes_settings(self): + """Update dockwidgets features settings""" + # Update toggle action on menu + for child in self.widgetlist: + features = child.FEATURES + if CONF.get('main', 'vertical_dockwidget_titlebars'): + features = features | QDockWidget.DockWidgetVerticalTitleBar + if not self.dockwidgets_locked: + features = features | QDockWidget.DockWidgetMovable + child.dockwidget.setFeatures(features) + child.update_margins() + + def apply_statusbar_settings(self): + """Update status bar widgets settings""" + show_status_bar = CONF.get('main', 'show_status_bar') + self.statusBar().setVisible(show_status_bar) + + if show_status_bar: + for widget, name in ((self.mem_status, 'memory_usage'), + (self.cpu_status, 'cpu_usage')): + if widget is not None: + widget.setVisible(CONF.get('main', '%s/enable' % name)) + widget.set_interval(CONF.get('main', '%s/timeout' % name)) + else: + return + + @Slot() + def edit_preferences(self): + """Edit Spyder preferences""" + from spyder.plugins.configdialog import ConfigDialog + dlg = ConfigDialog(self) + dlg.size_change.connect(self.set_prefs_size) + if self.prefs_dialog_size is not None: + dlg.resize(self.prefs_dialog_size) + for PrefPageClass in self.general_prefs: + widget = PrefPageClass(dlg, main=self) + widget.initialize() + dlg.add_page(widget) + for plugin in [self.workingdirectory, self.editor, + self.projects, self.extconsole, self.ipyconsole, + self.historylog, self.help, self.variableexplorer, + self.onlinehelp, self.explorer, self.findinfiles + ]+self.thirdparty_plugins: + if plugin is not None: + try: + widget = plugin.create_configwidget(dlg) + if widget is not None: + dlg.add_page(widget) + except Exception: + traceback.print_exc(file=sys.stderr) + if self.prefs_index is not None: + dlg.set_current_index(self.prefs_index) + dlg.show() + dlg.check_all_settings() + dlg.pages_widget.currentChanged.connect(self.__preference_page_changed) + dlg.exec_() + + def __preference_page_changed(self, index): + """Preference page index has changed""" + self.prefs_index = index + + def set_prefs_size(self, size): + """Save preferences dialog size""" + self.prefs_dialog_size = size + + #---- Shortcuts + def register_shortcut(self, qaction_or_qshortcut, context, name, + add_sc_to_tip=False): + """ + Register QAction or QShortcut to Spyder main application, + with shortcut (context, name, default) + """ + self.shortcut_data.append( (qaction_or_qshortcut, context, + name, add_sc_to_tip) ) + + def apply_shortcuts(self): + """Apply shortcuts settings to all widgets/plugins""" + toberemoved = [] + for index, (qobject, context, name, + add_sc_to_tip) in enumerate(self.shortcut_data): + keyseq = QKeySequence( get_shortcut(context, name) ) + try: + if isinstance(qobject, QAction): + if sys.platform == 'darwin' and \ + qobject._shown_shortcut == 'missing': + qobject._shown_shortcut = keyseq + else: + qobject.setShortcut(keyseq) + if add_sc_to_tip: + add_shortcut_to_tooltip(qobject, context, name) + elif isinstance(qobject, QShortcut): + qobject.setKey(keyseq) + except RuntimeError: + # Object has been deleted + toberemoved.append(index) + for index in sorted(toberemoved, reverse=True): + self.shortcut_data.pop(index) + + # -- Open files server + def start_open_files_server(self): + self.open_files_server.setsockopt(socket.SOL_SOCKET, + socket.SO_REUSEADDR, 1) + port = select_port(default_port=OPEN_FILES_PORT) + CONF.set('main', 'open_files_port', port) + self.open_files_server.bind(('127.0.0.1', port)) + self.open_files_server.listen(20) + while 1: # 1 is faster than True + try: + req, dummy = self.open_files_server.accept() + except socket.error as e: + # See Issue 1275 for details on why errno EINTR is + # silently ignored here. + eintr = errno.WSAEINTR if os.name == 'nt' else errno.EINTR + # To avoid a traceback after closing on Windows + if e.args[0] == eintr: + continue + # handle a connection abort on close error + enotsock = (errno.WSAENOTSOCK if os.name == 'nt' + else errno.ENOTSOCK) + if e.args[0] in [errno.ECONNABORTED, enotsock]: + return + raise + fname = req.recv(1024) + fname = fname.decode('utf-8') + self.sig_open_external_file.emit(fname) + req.sendall(b' ') + + # ---- Quit and restart, and reset spyder defaults + @Slot() + def reset_spyder(self): + """ + Quit and reset Spyder and then Restart application. + """ + answer = QMessageBox.warning(self, _("Warning"), + _("Spyder will restart and reset to default settings:

" + "Do you want to continue?"), + QMessageBox.Yes | QMessageBox.No) + if answer == QMessageBox.Yes: + self.restart(reset=True) + + @Slot() + def restart(self, reset=False): + """ + Quit and Restart Spyder application. + + If reset True it allows to reset spyder on restart. + """ + # Get start path to use in restart script + spyder_start_directory = get_module_path('spyder') + restart_script = osp.join(spyder_start_directory, 'app', 'restart.py') + + # Get any initial argument passed when spyder was started + # Note: Variables defined in bootstrap.py and spyder/app/start.py + env = os.environ.copy() + bootstrap_args = env.pop('SPYDER_BOOTSTRAP_ARGS', None) + spyder_args = env.pop('SPYDER_ARGS') + + # Get current process and python running spyder + pid = os.getpid() + python = sys.executable + + # Check if started with bootstrap.py + if bootstrap_args is not None: + spyder_args = bootstrap_args + is_bootstrap = True + else: + is_bootstrap = False + + # Pass variables as environment variables (str) to restarter subprocess + env['SPYDER_ARGS'] = spyder_args + env['SPYDER_PID'] = str(pid) + env['SPYDER_IS_BOOTSTRAP'] = str(is_bootstrap) + env['SPYDER_RESET'] = str(reset) + + if DEV: + if os.name == 'nt': + env['PYTHONPATH'] = ';'.join(sys.path) + else: + env['PYTHONPATH'] = ':'.join(sys.path) + + # Build the command and popen arguments depending on the OS + if os.name == 'nt': + # Hide flashing command prompt + startupinfo = subprocess.STARTUPINFO() + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + shell = False + else: + startupinfo = None + shell = True + + command = '"{0}" "{1}"' + command = command.format(python, restart_script) + + try: + if self.closing(True): + subprocess.Popen(command, shell=shell, env=env, + startupinfo=startupinfo) + self.console.quit() + except Exception as error: + # If there is an error with subprocess, Spyder should not quit and + # the error can be inspected in the internal console + print(error) + print(command) + + # ---- Interactive Tours + def show_tour(self, index): + """ """ + frames = self.tours_available[index] + self.tour.set_tour(index, frames, self) + self.tour.start_tour() + + # ---- Check for Spyder Updates + def _check_updates_ready(self): + """Called by WorkerUpdates when ready""" + from spyder.widgets.helperwidgets import MessageCheckBox + + # feedback` = False is used on startup, so only positive feedback is + # given. `feedback` = True is used when after startup (when using the + # menu action, and gives feeback if updates are, or are not found. + feedback = self.give_updates_feedback + + # Get results from worker + update_available = self.worker_updates.update_available + latest_release = self.worker_updates.latest_release + error_msg = self.worker_updates.error + + url_r = 'https://github.com/spyder-ide/spyder/releases' + url_i = 'http://pythonhosted.org/spyder/installation.html' + + # Define the custom QMessageBox + box = MessageCheckBox() + box.setWindowTitle(_("Spyder updates")) + box.set_checkbox_text(_("Check for updates on startup")) + box.setStandardButtons(QMessageBox.Ok) + box.setDefaultButton(QMessageBox.Ok) + #The next line is commented because it freezes the dialog. + #For now there is then no info icon. This solves issue #3609. + #box.setIcon(QMessageBox.Information) + + # Adjust the checkbox depending on the stored configuration + section, option = 'main', 'check_updates_on_startup' + check_updates = CONF.get(section, option) + box.set_checked(check_updates) + + if error_msg is not None: + msg = error_msg + box.setText(msg) + box.set_check_visible(False) + box.exec_() + check_updates = box.is_checked() + else: + if update_available: + msg = _("Spyder %s is available!

Please use " + "your package manager to update Spyder or go to our " + "Releases page to download this " + "new version.

If you are not sure how to " + "proceed to update Spyder please refer to our " + " Installation instructions." + "") % (latest_release, url_r, url_i) + box.setText(msg) + box.set_check_visible(True) + box.exec_() + check_updates = box.is_checked() + elif feedback: + msg = _("Spyder is up to date.") + box.setText(msg) + box.set_check_visible(False) + box.exec_() + check_updates = box.is_checked() + + # Update checkbox based on user interaction + CONF.set(section, option, check_updates) + + # Enable check_updates_action after the thread has finished + self.check_updates_action.setDisabled(False) + + # Provide feeback when clicking menu if check on startup is on + self.give_updates_feedback = True + + @Slot() + def check_updates(self): + """ + Check for spyder updates on github releases using a QThread. + """ + from spyder.workers.updates import WorkerUpdates + + # Disable check_updates_action while the thread is working + self.check_updates_action.setDisabled(True) + + if self.thread_updates is not None: + self.thread_updates.terminate() + + self.thread_updates = QThread(self) + self.worker_updates = WorkerUpdates(self) + self.worker_updates.sig_ready.connect(self._check_updates_ready) + self.worker_updates.sig_ready.connect(self.thread_updates.quit) + self.worker_updates.moveToThread(self.thread_updates) + self.thread_updates.started.connect(self.worker_updates.start) + self.thread_updates.start() + + +#============================================================================== +# Utilities to create the 'main' function +#============================================================================== +def initialize(): + """Initialize Qt, patching sys.exit and eventually setting up ETS""" + # This doesn't create our QApplication, just holds a reference to + # MAIN_APP, created above to show our splash screen as early as + # possible + app = qapplication() + + #----Monkey patching QApplication + class FakeQApplication(QApplication): + """Spyder's fake QApplication""" + def __init__(self, args): + self = app # analysis:ignore + @staticmethod + def exec_(): + """Do nothing because the Qt mainloop is already running""" + pass + from qtpy import QtWidgets + QtWidgets.QApplication = FakeQApplication + + #----Monkey patching rope + try: + from spyder import rope_patch + rope_patch.apply() + except ImportError: + # rope is not installed + pass + + #----Monkey patching sys.exit + def fake_sys_exit(arg=[]): + pass + sys.exit = fake_sys_exit + + #----Monkey patching sys.excepthook to avoid crashes in PyQt 5.5+ + if PYQT5: + def spy_excepthook(type_, value, tback): + sys.__excepthook__(type_, value, tback) + sys.excepthook = spy_excepthook + + # Removing arguments from sys.argv as in standard Python interpreter + sys.argv = [''] + + # Selecting Qt4 backend for Enthought Tool Suite (if installed) + try: + from enthought.etsconfig.api import ETSConfig + ETSConfig.toolkit = 'qt4' + except ImportError: + pass + + return app + + +class Spy(object): + """ + Inspect Spyder internals + + Attributes: + app Reference to main QApplication object + window Reference to spyder.MainWindow widget + """ + def __init__(self, app, window): + self.app = app + self.window = window + def __dir__(self): + return list(self.__dict__.keys()) +\ + [x for x in dir(self.__class__) if x[0] != '_'] + def versions(self): + return get_versions() + + +def run_spyder(app, options, args): + """ + Create and show Spyder's main window + Start QApplication event loop + """ + #TODO: insert here + # Main window + main = MainWindow(options) + try: + main.setup() + except BaseException: + if main.console is not None: + try: + main.console.shell.exit_interpreter() + except BaseException: + pass + raise + + main.show() + main.post_visible_setup() + + if main.console: + main.console.shell.interpreter.namespace['spy'] = \ + Spy(app=app, window=main) + + # Open external files passed as args + if args: + for a in args: + main.open_external_file(a) + + # Don't show icons in menus for Mac + if sys.platform == 'darwin': + QCoreApplication.setAttribute(Qt.AA_DontShowIconsInMenus, True) + + # Open external files with our Mac app + if running_in_mac_app(): + app.sig_open_external_file.connect(main.open_external_file) + + # To give focus again to the last focused widget after restoring + # the window + app.focusChanged.connect(main.change_last_focused_widget) + + app.exec_() + return main + + +#============================================================================== +# Main +#============================================================================== +def main(): + """Main function""" + + # **** Collect command line options **** + # Note regarding Options: + # It's important to collect options before monkey patching sys.exit, + # otherwise, optparse won't be able to exit if --help option is passed + options, args = get_options() + + if set_attached_console_visible is not None: + set_attached_console_visible(DEBUG or options.show_console \ + or options.reset_config_files \ + or options.reset_to_defaults \ + or options.optimize) + + app = initialize() + if options.reset_config_files: + # Remove all configuration files! + reset_config_files() + return + elif options.reset_to_defaults: + # Reset Spyder settings to defaults + CONF.reset_to_defaults(save=True) + return + elif options.optimize: + # Optimize the whole Spyder's source code directory + import spyder + programs.run_python_script(module="compileall", + args=[spyder.__path__[0]], p_args=['-O']) + return + + # Show crash dialog + if CONF.get('main', 'crash', False) and not DEV: + CONF.set('main', 'crash', False) + SPLASH.hide() + QMessageBox.information(None, "Spyder", + "Spyder crashed during last session.

" + "If Spyder does not start at all and before submitting a " + "bug report, please try to reset settings to defaults by " + "running Spyder with the command line option '--reset':
" + "python spyder --reset" + "

" + "Warning: " + "this command will remove all your Spyder configuration files " + "located in '%s').

" + "If restoring the default settings does not help, please take " + "the time to search for known bugs or " + "discussions matching your situation before " + "eventually creating a new issue here. " + "Your feedback will always be greatly appreciated." + "" % (get_conf_path(), __project_url__, + __forum_url__, __project_url__)) + + # Create main window + mainwindow = None + try: + mainwindow = run_spyder(app, options, args) + except BaseException: + CONF.set('main', 'crash', True) + import traceback + traceback.print_exc(file=STDERR) + traceback.print_exc(file=open('spyder_crash.log', 'w')) + if mainwindow is None: + # An exception occured + SPLASH.hide() + return + + ORIGINAL_SYS_EXIT() + + +if __name__ == "__main__": + main() diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/app/restart.py spyder-3.0.2+dfsg1/spyder/app/restart.py --- spyder-2.3.8+dfsg1/spyder/app/restart.py 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/app/restart.py 2016-10-25 02:05:22.000000000 +0200 @@ -0,0 +1,279 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright © Spyder Project Contributors +# Licensed under the terms of the MIT License +# (see spyder/__init__.py for details) + +""" +Restart Spyder + +A helper script that allows to restart (and also reset) Spyder from within the +running application. +""" + +# Standard library imports +import ast +import os +import os.path as osp +import subprocess +import sys +import time + +# Third party imports +from qtpy.QtCore import Qt, QTimer +from qtpy.QtGui import QColor, QPixmap +from qtpy.QtWidgets import QApplication, QMessageBox, QSplashScreen, QWidget + +# Local imports +from spyder.config.base import _, get_image_path +from spyder.py3compat import to_text_string +from spyder.utils import icon_manager as ima +from spyder.utils.qthelpers import qapplication + + +PY2 = sys.version[0] == '2' +IS_WINDOWS = os.name == 'nt' +SLEEP_TIME = 0.2 # Seconds for throttling control +CLOSE_ERROR, RESET_ERROR, RESTART_ERROR = [1, 2, 3] # Spyder error codes + + +def _is_pid_running_on_windows(pid): + """Check if a process is running on windows systems based on the pid.""" + pid = str(pid) + + # Hide flashing command prompt + startupinfo = subprocess.STARTUPINFO() + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + + process = subprocess.Popen(r'tasklist /fi "PID eq {0}"'.format(pid), + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + startupinfo=startupinfo) + stdoutdata, stderrdata = process.communicate() + stdoutdata = to_text_string(stdoutdata) + process.kill() + check = pid in stdoutdata + + return check + + +def _is_pid_running_on_unix(pid): + """Check if a process is running on unix systems based on the pid.""" + try: + # On unix systems os.kill with a 0 as second argument only pokes the + # process (if it exists) and does not kill it + os.kill(pid, 0) + except OSError: + return False + else: + return True + + +def is_pid_running(pid): + """Check if a process is running based on the pid.""" + # Select the correct function depending on the OS + if os.name == 'nt': + return _is_pid_running_on_windows(pid) + else: + return _is_pid_running_on_unix(pid) + + +class Restarter(QWidget): + """Widget in charge of displaying the splash information screen and the + error messages. + """ + def __init__(self): + super(Restarter, self).__init__() + self.ellipsis = ['', '.', '..', '...', '..', '.'] + + # Widgets + self.timer_ellipsis = QTimer(self) + self.splash = QSplashScreen(QPixmap(get_image_path('splash.svg'), + 'svg')) + + # Widget setup + self.setVisible(False) + + font = self.splash.font() + font.setPixelSize(10) + self.splash.setFont(font) + self.splash.show() + + self.timer_ellipsis.timeout.connect(self.animate_ellipsis) + + def _show_message(self, text): + """Show message on splash screen.""" + self.splash.showMessage(text, Qt.AlignBottom | Qt.AlignCenter | + Qt.AlignAbsolute, QColor(Qt.white)) + + def animate_ellipsis(self): + """Animate dots at the end of the splash screen message.""" + ellipsis = self.ellipsis.pop(0) + text = ' '*len(ellipsis) + self.splash_text + ellipsis + self.ellipsis.append(ellipsis) + self._show_message(text) + + def set_splash_message(self, text): + """Sets the text in the bottom of the Splash screen.""" + self.splash_text = text + self._show_message(text) + self.timer_ellipsis.start(500) + + def launch_error_message(self, error_type, error=None): + """Launch a message box with a predefined error message. + + Parameters + ---------- + error_type : int [CLOSE_ERROR, RESET_ERROR, RESTART_ERROR] + Possible error codes when restarting/reseting spyder. + error : Exception + Actual Python exception error caught. + """ + messages = {CLOSE_ERROR: _("It was not possible to close the previous " + "Spyder instance.\nRestart aborted."), + RESET_ERROR: _("Spyder could not reset to factory " + "defaults.\nRestart aborted."), + RESTART_ERROR: _("It was not possible to restart Spyder.\n" + "Operation aborted.")} + titles = {CLOSE_ERROR: _("Spyder exit error"), + RESET_ERROR: _("Spyder reset error"), + RESTART_ERROR: _("Spyder restart error")} + + if error: + e = error.__repr__() + message = messages[error_type] + "\n\n{0}".format(e) + else: + message = messages[error_type] + + title = titles[error_type] + self.splash.hide() + QMessageBox.warning(self, title, message, QMessageBox.Ok) + raise RuntimeError(message) + + +def main(): + # Splash screen + # ------------------------------------------------------------------------- + # Start Qt Splash to inform the user of the current status + app = qapplication() + restarter = Restarter() + resample = not IS_WINDOWS + # Resampling SVG icon only on non-Windows platforms (see Issue 1314): + icon = ima.icon('spyder', resample=resample) + app.setWindowIcon(icon) + restarter.set_splash_message(_('Closing Spyder')) + + # Get variables + # Note: Variables defined in app/spyder.py 'restart()' method + spyder_args = os.environ.pop('SPYDER_ARGS', None) + pid = os.environ.pop('SPYDER_PID', None) + is_bootstrap = os.environ.pop('SPYDER_IS_BOOTSTRAP', None) + reset = os.environ.pop('SPYDER_RESET', None) + + # Get the spyder base folder based on this file + this_folder = osp.split(osp.dirname(osp.abspath(__file__)))[0] + spyder_folder = osp.split(this_folder)[0] + + if not any([spyder_args, pid, is_bootstrap, reset]): + error = "This script can only be called from within a Spyder instance" + raise RuntimeError(error) + + # Variables were stored as string literals in the environment, so to use + # them we need to parse them in a safe manner. + is_bootstrap = ast.literal_eval(is_bootstrap) + pid = ast.literal_eval(pid) + args = ast.literal_eval(spyder_args) + reset = ast.literal_eval(reset) + + # Enforce the --new-instance flag when running spyder + if '--new-instance' not in args: + if is_bootstrap and '--' not in args: + args = args + ['--', '--new-instance'] + else: + args.append('--new-instance') + + # Create the arguments needed for reseting + if '--' in args: + args_reset = ['--', '--reset'] + else: + args_reset = ['--reset'] + + # Arrange arguments to be passed to the restarter and reset subprocess + args = ' '.join(args) + args_reset = ' '.join(args_reset) + + # Get python excutable running this script + python = sys.executable + + # Build the command + if is_bootstrap: + spyder = osp.join(spyder_folder, 'bootstrap.py') + else: + spyderdir = osp.join(spyder_folder, 'spyder') + spyder = osp.join(spyderdir, 'app', 'start.py') + + command = '"{0}" "{1}" {2}'.format(python, spyder, args) + + # Adjust the command and/or arguments to subprocess depending on the OS + shell = not IS_WINDOWS + + # Before launching a new Spyder instance we need to make sure that the + # previous one has closed. We wait for a fixed and "reasonable" amount of + # time and check, otherwise an error is launched + wait_time = 90 if IS_WINDOWS else 30 # Seconds + for counter in range(int(wait_time/SLEEP_TIME)): + if not is_pid_running(pid): + break + time.sleep(SLEEP_TIME) # Throttling control + QApplication.processEvents() # Needed to refresh the splash + else: + # The old spyder instance took too long to close and restart aborts + restarter.launch_error_message(error_type=CLOSE_ERROR) + + env = os.environ.copy() + + # Reset Spyder (if required) + # ------------------------------------------------------------------------- + if reset: + restarter.set_splash_message(_('Resetting Spyder to defaults')) + command_reset = '"{0}" "{1}" {2}'.format(python, spyder, args_reset) + + try: + p = subprocess.Popen(command_reset, shell=shell, env=env) + except Exception as error: + restarter.launch_error_message(error_type=RESET_ERROR, error=error) + else: + p.communicate() + pid_reset = p.pid + + # Before launching a new Spyder instance we need to make sure that the + # reset subprocess has closed. We wait for a fixed and "reasonable" + # amount of time and check, otherwise an error is launched. + wait_time = 20 # Seconds + for counter in range(int(wait_time/SLEEP_TIME)): + if not is_pid_running(pid_reset): + break + time.sleep(SLEEP_TIME) # Throttling control + QApplication.processEvents() # Needed to refresh the splash + else: + # The reset subprocess took too long and it is killed + try: + p.kill() + except OSError as error: + restarter.launch_error_message(error_type=RESET_ERROR, + error=error) + else: + restarter.launch_error_message(error_type=RESET_ERROR) + + # Restart + # ------------------------------------------------------------------------- + restarter.set_splash_message(_('Restarting')) + try: + subprocess.Popen(command, shell=shell, env=env) + except Exception as error: + restarter.launch_error_message(error_type=RESTART_ERROR, error=error) + + +if __name__ == '__main__': + main() diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/app/start.py spyder-3.0.2+dfsg1/spyder/app/start.py --- spyder-2.3.8+dfsg1/spyder/app/start.py 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/app/start.py 2016-10-25 02:05:22.000000000 +0200 @@ -0,0 +1,119 @@ +# -*- coding: utf-8 -*- + +# Std imports +import os +import os.path as osp +import random +import socket +import sys +import time + +# Local imports +from spyder.app.cli_options import get_options +from spyder.config.base import get_conf_path, running_in_mac_app +from spyder.config.main import CONF +from spyder.utils.external import lockfile +from spyder.py3compat import is_unicode + + +def send_args_to_spyder(args): + """ + Simple socket client used to send the args passed to the Spyder + executable to an already running instance. + + Args can be Python scripts or files with these extensions: .spydata, .mat, + .npy, or .h5, which can be imported by the Variable Explorer. + """ + port = CONF.get('main', 'open_files_port') + + # Wait ~50 secs for the server to be up + # Taken from http://stackoverflow.com/a/4766598/438386 + for _x in range(200): + try: + for arg in args: + client = socket.socket(socket.AF_INET, socket.SOCK_STREAM, + socket.IPPROTO_TCP) + client.connect(("127.0.0.1", port)) + if is_unicode(arg): + arg = arg.encode('utf-8') + client.send(osp.abspath(arg)) + client.close() + except socket.error: + time.sleep(0.25) + continue + break + + +def main(): + """ + Start Spyder application. + + If single instance mode is turned on (default behavior) and an instance of + Spyder is already running, this will just parse and send command line + options to the application. + """ + # Parse command line options + options, args = get_options() + + # Store variable to be used in self.restart (restart spyder instance) + os.environ['SPYDER_ARGS'] = str(sys.argv[1:]) + + if CONF.get('main', 'single_instance') and not options.new_instance \ + and not options.reset_config_files and not running_in_mac_app(): + # Minimal delay (0.1-0.2 secs) to avoid that several + # instances started at the same time step in their + # own foots while trying to create the lock file + time.sleep(random.randrange(1000, 2000, 90)/10000.) + + # Lock file creation + lock_file = get_conf_path('spyder.lock') + lock = lockfile.FilesystemLock(lock_file) + + # Try to lock spyder.lock. If it's *possible* to do it, then + # there is no previous instance running and we can start a + # new one. If *not*, then there is an instance already + # running, which is locking that file + try: + lock_created = lock.lock() + except: + # If locking fails because of errors in the lockfile + # module, try to remove a possibly stale spyder.lock. + # This is reported to solve all problems with + # lockfile (See issue 2363) + try: + if os.name == 'nt': + if osp.isdir(lock_file): + import shutil + shutil.rmtree(lock_file, ignore_errors=True) + else: + if osp.islink(lock_file): + os.unlink(lock_file) + except: + pass + + # Then start Spyder as usual and *don't* continue + # executing this script because it doesn't make + # sense + from spyder.app import mainwindow + mainwindow.main() + return + + if lock_created: + # Start a new instance + from spyder.app import mainwindow + mainwindow.main() + else: + # Pass args to Spyder or print an informative + # message + if args: + send_args_to_spyder(args) + else: + print("Spyder is already running. If you want to open a new \n" + "instance, please pass to it the --new-instance option") + else: + from spyder.app import mainwindow + mainwindow.main() + + +if __name__ == "__main__": + main() diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/app/tour.py spyder-3.0.2+dfsg1/spyder/app/tour.py --- spyder-2.3.8+dfsg1/spyder/app/tour.py 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/app/tour.py 2016-10-25 02:05:22.000000000 +0200 @@ -0,0 +1,1289 @@ +# -*- coding: utf-8 -*- +# +# Copyright © Spyder Project Contributors +# Licensed under the terms of the MIT License +# (see spyder/__init__.py for details) + +"""Spyder interactive tours""" + +# pylint: disable=C0103 +# pylint: disable=R0903 +# pylint: disable=R0911 +# pylint: disable=R0201 + +# Standard library imports +from __future__ import division + +import sys + +# Third party imports +from qtpy.QtCore import (QEasingCurve, QPoint, QPropertyAnimation, QRectF, Qt, + Signal) +from qtpy.QtGui import (QBrush, QColor, QIcon, QPainter, QPainterPath, QPen, + QPixmap, QRegion) +from qtpy.QtWidgets import (QAction, QApplication, QComboBox, QDialog, + QGraphicsOpacityEffect, QHBoxLayout, QLabel, + QLayout, QMainWindow, QMenu, QPushButton, + QSpacerItem, QToolButton, QVBoxLayout, QWidget) + +# Local imports +from spyder.config.base import _, get_image_path +from spyder.py3compat import to_binary_string +from spyder.utils.qthelpers import add_actions, create_action + +# FIXME: Known issues +# How to handle if an specific dockwidget does not exists/load, like ipython +# on python3.3, should that frame be removed? should it display a warning? + +class SpyderWidgets(object): + """List of supported widgets to highlight/decorate""" + # Panes + external_console = 'extconsole' + ipython_console = 'ipyconsole' + editor = 'editor' + editor_line_number_area = 'editor.get_current_editor().linenumberarea' + editor_scroll_flag_area = 'editor.get_current_editor().scrollflagarea' + file_explorer = 'explorer' + help_plugin = 'help' + variable_explorer = 'variableexplorer' + history_log = "historylog" + + # Toolbars + toolbars = '' + toolbars_active = '' + toolbar_file = '' + toolbar_edit = '' + toolbar_run = '' + toolbar_debug = '' + toolbar_main = '' + + status_bar = '' + menu_bar = '' + menu_file = '' + menu_edit = '' + + +def get_tours(index=None): + """ + Get the list of available tours (if index=None), or the your given by + index + """ + return get_tour(index) + + +def get_tour(index): + """ + This function generates a list of tours. + + The index argument is used to retrieve a particular tour. If None is + passed, it will return the full list of tours. If instead -1 is given, + this function will return a test tour + + To add more tours a new variable needs to be created to hold the list of + dicts and the tours variable at the bottom of this function needs to be + updated accordingly + """ + sw = SpyderWidgets + qtconsole_link = "http://ipython.org/ipython-doc/stable/interactive/qtconsole.html" + + # This test should serve as example of keys to use in the tour frame dics + test = [{'title': "Welcome to Spyder introduction tour", + 'content': "Spyder is an interactive development \ + environment. This tip panel supports rich text.
\ +
it also supports image insertion to the right so\ + far", + 'image': 'tour-spyder-logo.png'}, + + {'title': "Widget display", + 'content': ("This show how a widget is displayed. The tip panel " + "is adjusted based on the first widget in the list"), + 'widgets': ['button1'], + 'decoration': ['button2'], + 'interact': True}, + + {'title': "Widget display", + 'content': ("This show how a widget is displayed. The tip panel " + "is adjusted based on the first widget in the list"), + 'widgets': ['button1'], + 'decoration': ['button1'], + 'interact': True}, + + {'title': "Widget display", + 'content': ("This show how a widget is displayed. The tip panel " + "is adjusted based on the first widget in the list"), + 'widgets': ['button1'], + 'interact': True}, + + {'title': "Widget display and highlight", + 'content': "This shows how a highlighted widget looks", + 'widgets': ['button'], + 'decoration': ['button'], + 'interact': False}, + ] + + intro = [{'title': _("Welcome to the Introduction tour"), + 'content': _("Spyder is a powerful Interactive " + "Development Environment (or IDE) for the Python " + "programming language.

" + "Here we are going to guide you through its most " + "important features.

" + "Please use the arrow keys or click on the buttons " + "below to move along the tour."), + 'image': 'tour-spyder-logo.png'}, + + {'title': _("The Editor"), + 'content': _("This is the pane where you write Python code before " + "evaluating it. You can get automatic suggestions " + "and completions while writing, by pressing the " + "Tab key next to a given text.

" + "The Editor comes " + "with a line number area (highlighted here in red), " + "where Spyder shows warnings and syntax errors. They " + "can help you to detect potential problems before " + "running the code.

" + "You can also set debug breakpoints in the line " + "number area, by doing a double click next to " + "a non-empty line."), + 'widgets': [sw.editor], + 'decoration': [sw.editor_line_number_area]}, + + {'title': _("The IPython console"), + 'content': _("This is one of panes where you can run or " + "execute the code you wrote on the Editor. To do it " + "you need to press the F5 key.

" + "This console comes with several " + "useful features that greatly improve your " + "programming workflow (like syntax highlighting and " + "inline plots). If you want to know more about them, " + "please follow this link.

" + "Please click on the button below to run some simple " + "code in this console. This will be useful to show " + "you other important features.").format( + qtconsole_link), + 'widgets': [sw.ipython_console], + 'run': ["li = list(range(100))", "d = {'a': 1, 'b': 2}"] + }, + + {'title': _("The Variable Explorer"), + 'content': _("In this pane you can view and edit the variables " + "generated during the execution of a program, or " + "those entered directly in one of Spyder " + "consoles.

" + "As you can see, the Variable Explorer is showing " + "the variables generated during the last step of " + "this tour. By doing a double-click on any " + "of them, a new window will be opened, where you " + "can inspect and modify their contents."), + 'widgets': [sw.variable_explorer], + 'interact': True}, + + {'title': _("The Python console"), + 'content': _("You can also run your code on a Python console. " + "These consoles are useful because they let you " + "run a file in a console dedicated only to it." + "To select this behavior, please press the F6 " + "key.

" + "By pressing the button below and then focusing the " + "Variable Explorer, you will notice that " + "Python consoles are also connected to that pane, " + "and that the Variable Explorer only shows " + "the variables of the currently focused console."), + 'widgets': [sw.external_console], + 'run': ["a = 2", "s='Hello, world!'"], + }, + + {'title': _("Help"), + 'content': _("This pane displays documentation of the " + "functions, classes, methods or modules you are " + "currently using in the Editor or the Consoles.

" + "To use it, you need to press Ctrl+I in " + "front of an object. If that object has some " + "documentation associated with it, it will be " + "displayed here."), + 'widgets': [sw.help_plugin], + 'interact': True}, + + {'title': _("The File Explorer"), + 'content': _("This pane lets you navigate through the directories " + "and files present in your computer.

" + "You can also open any of these files with its " + "corresponding application, by doing a double " + "click on it.

" + "There is one exception to this rule: plain-text " + "files will always be opened in the Spyder Editor."), + 'widgets': [sw.file_explorer], + 'interact': True}, + + {'title': _("The History Log"), + 'content': _("This pane records all commands introduced in " + "the Python and IPython consoles."), + 'widgets': [sw.history_log], + 'interact': True}, + ] + +# ['The run toolbar', +# 'Should be short', +# ['self.run_toolbar'], None], +# ['The debug toolbar', +# '', +# ['self.debug_toolbar'], None], +# ['The main toolbar', +# '', +# ['self.main_toolbar'], None], +# ['The editor', +# 'Spyder has differnet bla bla bla', +# ['self.editor.dockwidget'], None], +# ['The editor', +# 'Spyder has differnet bla bla bla', +# ['self.outlineexplorer.dockwidget'], None], +# +# ['The menu bar', +# 'Spyder has differnet bla bla bla', +# ['self.menuBar()'], None], +# +# ['The menu bar', +# 'Spyder has differnet bla bla bla', +# ['self.statusBar()'], None], +# +# +# ['The toolbars!', +# 'Spyder has differnet bla bla bla', +# ['self.variableexplorer.dockwidget'], None], +# ['The toolbars MO!', +# 'Spyder has differnet bla bla bla', +# ['self.extconsole.dockwidget'], None], +# ['The whole window?!', +# 'Spyder has differnet bla bla bla', +# ['self'], None], +# ['Lets try something!', +# 'Spyder has differnet bla bla bla', +# ['self.extconsole.dockwidget', +# 'self.variableexplorer.dockwidget'], None] +# +# ] + + feat30 = [{'title': "New features in Spyder 3.0", + 'content': _("Spyder is an interactive development " + "environment based on bla"), + 'image': 'spyder.png'}, + + {'title': _("Welcome to Spyder introduction tour"), + 'content': _("Spyder is an interactive development environment " + "based on bla"), + 'widgets': ['variableexplorer']}, + ] + + tours = [{'name': _('Introduction tour'), 'tour': intro}, + {'name': _('New features in version 3.0'), 'tour': feat30}] + + if index is None: + return tours + elif index == -1: + return [test] + else: + return [tours[index]] + + +class FadingDialog(QDialog): + """A general fade in/fade out QDialog with some builtin functions""" + sig_key_pressed = Signal() + + def __init__(self, parent, opacity, duration, easing_curve): + super(FadingDialog, self).__init__(parent) + + self.parent = parent + self.opacity_min = min(opacity) + self.opacity_max = max(opacity) + self.duration_fadein = duration[0] + self.duration_fadeout = duration[-1] + self.easing_curve_in = easing_curve[0] + self.easing_curve_out = easing_curve[-1] + self.effect = None + self.anim = None + + self._fade_running = False + self._funcs_before_fade_in = [] + self._funcs_after_fade_in = [] + self._funcs_before_fade_out = [] + self._funcs_after_fade_out = [] + + self.setModal(False) + + def _run(self, funcs): + """ """ + for func in funcs: + func() + + def _run_before_fade_in(self): + """ """ + self._run(self._funcs_before_fade_in) + + def _run_after_fade_in(self): + """ """ + self._run(self._funcs_after_fade_in) + + def _run_before_fade_out(self): + """ """ + self._run(self._funcs_before_fade_out) + + def _run_after_fade_out(self): + """ """ + self._run(self._funcs_after_fade_out) + + def _set_fade_finished(self): + """ """ + self._fade_running = False + + def _fade_setup(self): + """ """ + self._fade_running = True + self.effect = QGraphicsOpacityEffect(self) + self.setGraphicsEffect(self.effect) + self.anim = QPropertyAnimation(self.effect, to_binary_string("opacity")) + + # --- public api + def fade_in(self, on_finished_connect): + """ """ + self._run_before_fade_in() + self._fade_setup() + self.show() + self.raise_() + self.anim.setEasingCurve(self.easing_curve_in) + self.anim.setStartValue(self.opacity_min) + self.anim.setEndValue(self.opacity_max) + self.anim.setDuration(self.duration_fadein) + self.anim.finished.connect(on_finished_connect) + self.anim.finished.connect(self._set_fade_finished) + self.anim.finished.connect(self._run_after_fade_in) + self.anim.start() + + def fade_out(self, on_finished_connect): + """ """ + self._run_before_fade_out() + self._fade_setup() + self.anim.setEasingCurve(self.easing_curve_out) + self.anim.setStartValue(self.opacity_max) + self.anim.setEndValue(self.opacity_min) + self.anim.setDuration(self.duration_fadeout) + self.anim.finished.connect(on_finished_connect) + self.anim.finished.connect(self._set_fade_finished) + self.anim.finished.connect(self._run_after_fade_out) + self.anim.start() + + def is_fade_running(self): + """ """ + return self._fade_running + + def set_funcs_before_fade_in(self, funcs): + """ """ + self._funcs_before_fade_in = funcs + + def set_funcs_after_fade_in(self, funcs): + """ """ + self._funcs_after_fade_in = funcs + + def set_funcs_before_fade_out(self, funcs): + """ """ + self._funcs_before_fade_out = funcs + + def set_funcs_after_fade_out(self, funcs): + """ """ + self._funcs_after_fade_out = funcs + + +class FadingCanvas(FadingDialog): + """The black semi transparent canvas that covers the application""" + def __init__(self, parent, opacity, duration, easing_curve, color): + super(FadingCanvas, self).__init__(parent, opacity, duration, + easing_curve) + self.parent = parent + + self.color = color # Canvas color + self.color_decoration = Qt.red # Decoration color + self.stroke_decoration = 2 # width in pixels for decoration + + self.region_mask = None + self.region_subtract = None + self.region_decoration = None + + self.widgets = None # The widget to uncover + self.decoration = None # The widget to draw decoration + self.interaction_on = False + + self.path_current = None + self.path_subtract = None + self.path_full = None + self.path_decoration = None + + # widget setup + self.setWindowFlags(Qt.Dialog | Qt.FramelessWindowHint) + self.setAttribute(Qt.WA_TranslucentBackground) + self.setAttribute(Qt.WA_TransparentForMouseEvents) + self.setModal(False) + self.setFocusPolicy(Qt.NoFocus) + + self.set_funcs_before_fade_in([self.update_canvas]) + self.set_funcs_after_fade_out([lambda: self.update_widgets(None), + lambda: self.update_decoration(None)]) + + def set_interaction(self, value): + """ """ + self.interaction_on = value + + def update_canvas(self): + """ """ + w, h = self.parent.size().width(), self.parent.size().height() + + self.path_full = QPainterPath() + self.path_subtract = QPainterPath() + self.path_decoration = QPainterPath() + self.region_mask = QRegion(0, 0, w, h) + + self.path_full.addRect(0, 0, w, h) + + # Add the path + if self.widgets is not None: + for widget in self.widgets: + temp_path = QPainterPath() + # if widget is not found... find more general way to handle + if widget is not None: + widget.raise_() + widget.show() + geo = widget.frameGeometry() + width, height = geo.width(), geo.height() + point = widget.mapTo(self.parent, QPoint(0, 0)) + x, y = point.x(), point.y() + + temp_path.addRect(QRectF(x, y, width, height)) + + temp_region = QRegion(x, y, width, height) + + if self.interaction_on: + self.region_mask = self.region_mask.subtracted(temp_region) + self.path_subtract = self.path_subtract.united(temp_path) + + self.path_current = self.path_full.subtracted(self.path_subtract) + else: + self.path_current = self.path_full + + if self.decoration is not None: + for widget in self.decoration: + temp_path = QPainterPath() + widget.raise_() + widget.show() + geo = widget.frameGeometry() + width, height = geo.width(), geo.height() + point = widget.mapTo(self.parent, QPoint(0, 0)) + x, y = point.x(), point.y() + temp_path.addRect(QRectF(x, y, width, height)) + + temp_region_1 = QRegion(x-1, y-1, width+2, height+2) + temp_region_2 = QRegion(x+1, y+1, width-2, height-2) + temp_region = temp_region_1.subtracted(temp_region_2) + + if self.interaction_on: + self.region_mask = self.region_mask.united(temp_region) + + self.path_decoration = self.path_decoration.united(temp_path) + else: + self.path_decoration.addRect(0, 0, 0, 0) + + # Add a decoration stroke around widget + self.setMask(self.region_mask) + self.update() + self.repaint() + + def update_widgets(self, widgets): + """ """ + self.widgets = widgets + + def update_decoration(self, widgets): + """ """ + self.decoration = widgets + + def paintEvent(self, event): + """Override Qt method""" + painter = QPainter(self) + painter.setRenderHint(QPainter.Antialiasing) + # Decoration + painter.fillPath(self.path_current, QBrush(self.color)) + painter.strokePath(self.path_decoration, QPen(self.color_decoration, + self.stroke_decoration)) +# decoration_fill = QColor(self.color_decoration) +# decoration_fill.setAlphaF(0.25) +# painter.fillPath(self.path_decoration, decoration_fill) + + def reject(self): + """Override Qt method""" + if not self.is_fade_running(): + key = Qt.Key_Escape + self.key_pressed = key + self.sig_key_pressed.emit() + + def mousePressEvent(self, event): + """Override Qt method""" + pass + + +class FadingTipBox(FadingDialog): + """ """ + def __init__(self, parent, opacity, duration, easing_curve): + super(FadingTipBox, self).__init__(parent, opacity, duration, + easing_curve) + self.holder = self.anim # needed for qt to work + self.parent = parent + + self.frames = None + self.color_top = QColor.fromRgb(230, 230, 230) + self.color_back = QColor.fromRgb(255, 255, 255) + self.offset_shadow = 0 + self.fixed_width = 300 + + self.key_pressed = None + + self.setAttribute(Qt.WA_TranslucentBackground) + self.setWindowFlags(Qt.Dialog | Qt.FramelessWindowHint | + Qt.WindowStaysOnTopHint) + self.setModal(False) + + # Widgets + self.button_home = QPushButton("<<") + self.button_close = QPushButton("X") + self.button_previous = QPushButton(" < ") + self.button_end = QPushButton(">>") + self.button_next = QPushButton(" > ") + self.button_run = QPushButton(_('Run code')) + self.button_disable = None + self.button_current = QToolButton() + self.label_image = QLabel() + + self.label_title = QLabel() + self.combo_title = QComboBox() + self.label_current = QLabel() + self.label_content = QLabel() + + self.label_content.setMinimumWidth(self.fixed_width) + self.label_content.setMaximumWidth(self.fixed_width) + + self.label_current.setAlignment(Qt.AlignCenter) + + self.label_content.setWordWrap(True) + + self.widgets = [self.label_content, self.label_title, + self.label_current, self.combo_title, + self.button_close, self.button_run, self.button_next, + self.button_previous, self.button_end, + self.button_home, self.button_current] + + arrow = get_image_path('hide.png') + + self.stylesheet = '''QPushButton { + background-color: rgbs(200,200,200,100%); + color: rgbs(0,0,0,100%); + border-style: outset; + border-width: 1px; + border-radius: 3px; + border-color: rgbs(100,100,100,100%); + padding: 2px; + } + + QPushButton:hover { + background-color: rgbs(150, 150, 150, 100%); + } + + QPushButton:disabled { + background-color: rgbs(230,230,230,100%); + color: rgbs(200,200,200,100%); + border-color: rgbs(200,200,200,100%); + } + + QComboBox { + padding-left: 5px; + background-color: rgbs(230,230,230,100%); + border-width: 0px; + border-radius: 0px; + min-height:20px; + max-height:20px; + } + + QComboBox::drop-down { + subcontrol-origin: padding; + subcontrol-position: top left; + border-width: 0px; + } + + QComboBox::down-arrow { + image: url(''' + arrow + '''); + } + + ''' + # Windows fix, slashes should be always in unix-style + self.stylesheet = self.stylesheet.replace('\\', '/') + + for widget in self.widgets: + widget.setFocusPolicy(Qt.NoFocus) + widget.setStyleSheet(self.stylesheet) + + layout_top = QHBoxLayout() + layout_top.addWidget(self.combo_title) + layout_top.addStretch() + layout_top.addWidget(self.button_close) + layout_top.addSpacerItem(QSpacerItem(self.offset_shadow, + self.offset_shadow)) + + layout_content = QHBoxLayout() + layout_content.addWidget(self.label_content) + layout_content.addWidget(self.label_image) + layout_content.addSpacerItem(QSpacerItem(5, 5)) + + layout_run = QHBoxLayout() + layout_run.addStretch() + layout_run.addWidget(self.button_run) + layout_run.addStretch() + layout_run.addSpacerItem(QSpacerItem(self.offset_shadow, + self.offset_shadow)) + + layout_navigation = QHBoxLayout() + layout_navigation.addWidget(self.button_home) + layout_navigation.addWidget(self.button_previous) + layout_navigation.addStretch() + layout_navigation.addWidget(self.label_current) + layout_navigation.addStretch() + layout_navigation.addWidget(self.button_next) + layout_navigation.addWidget(self.button_end) + layout_navigation.addSpacerItem(QSpacerItem(self.offset_shadow, + self.offset_shadow)) + + layout = QVBoxLayout() + layout.addLayout(layout_top) + layout.addStretch() + layout.addSpacerItem(QSpacerItem(15, 15)) + layout.addLayout(layout_content) + layout.addLayout(layout_run) + layout.addStretch() + layout.addSpacerItem(QSpacerItem(15, 15)) + layout.addLayout(layout_navigation) + layout.addSpacerItem(QSpacerItem(self.offset_shadow, + self.offset_shadow)) + + layout.setSizeConstraint(QLayout.SetFixedSize) + + self.setLayout(layout) + + self.set_funcs_before_fade_in([self._disable_widgets]) + self.set_funcs_after_fade_in([self._enable_widgets]) + self.set_funcs_before_fade_out([self._disable_widgets]) + + self.setContextMenuPolicy(Qt.CustomContextMenu) + + # signals and slots + # These are defined every time by the AnimatedTour Class + + def _disable_widgets(self): + """ """ + for widget in self.widgets: + widget.setDisabled(True) + + def _enable_widgets(self): + """ """ + self.setWindowFlags(Qt.Dialog | Qt.FramelessWindowHint | + Qt.WindowStaysOnTopHint) + for widget in self.widgets: + widget.setDisabled(False) + + if self.button_disable == 'previous': + self.button_previous.setDisabled(True) + self.button_home.setDisabled(True) + elif self.button_disable == 'next': + self.button_next.setDisabled(True) + self.button_end.setDisabled(True) + + def set_data(self, title, content, current, image, run, frames=None, + step=None): + """ """ + self.label_title.setText(title) + self.combo_title.clear() + self.combo_title.addItems(frames) + self.combo_title.setCurrentIndex(step) +# min_content_len = max([len(f) for f in frames]) +# self.combo_title.setMinimumContentsLength(min_content_len) + + # Fix and try to see how it looks with a combo box + self.label_current.setText(current) + self.button_current.setText(current) + self.label_content.setText(content) + self.image = image + + if image is None: + self.label_image.setFixedHeight(1) + self.label_image.setFixedWidth(1) + else: + extension = image.split('.')[-1] + self.image = QPixmap(get_image_path(image), extension) + self.label_image.setPixmap(self.image) + self.label_image.setFixedSize(self.image.size()) + + if run is None: + self.button_run.setVisible(False) + else: + self.button_run.setDisabled(False) + self.button_run.setVisible(True) + + # Refresh layout + self.layout().activate() + + def set_pos(self, x, y): + """ """ + self.x = x + self.y = y + self.move(QPoint(x, y)) + + def build_paths(self): + """ """ + geo = self.geometry() + radius = 30 + shadow = self.offset_shadow + x0, y0 = geo.x(), geo.y() + width, height = geo.width() - shadow, geo.height() - shadow + + left, top = 0, 0 + right, bottom = width, height + + self.round_rect_path = QPainterPath() + self.round_rect_path.moveTo(right, top + radius) + self.round_rect_path.arcTo(right-radius, top, radius, radius, 0.0, + 90.0) + self.round_rect_path.lineTo(left+radius, top) + self.round_rect_path.arcTo(left, top, radius, radius, 90.0, 90.0) + self.round_rect_path.lineTo(left, bottom-radius) + self.round_rect_path.arcTo(left, bottom-radius, radius, radius, 180.0, + 90.0) + self.round_rect_path.lineTo(right-radius, bottom) + self.round_rect_path.arcTo(right-radius, bottom-radius, radius, radius, + 270.0, 90.0) + self.round_rect_path.closeSubpath() + + # Top path + header = 36 + offset = 2 + left, top = offset, offset + right = width - (offset) + self.top_rect_path = QPainterPath() + self.top_rect_path.lineTo(right, top + radius) + self.top_rect_path.moveTo(right, top + radius) + self.top_rect_path.arcTo(right-radius, top, radius, radius, 0.0, 90.0) + self.top_rect_path.lineTo(left+radius, top) + self.top_rect_path.arcTo(left, top, radius, radius, 90.0, 90.0) + self.top_rect_path.lineTo(left, top + header) + self.top_rect_path.lineTo(right, top + header) + + def paintEvent(self, event): + """ """ + self.build_paths() + + painter = QPainter(self) + painter.setRenderHint(QPainter.Antialiasing) + + painter.fillPath(self.round_rect_path, self.color_back) + painter.fillPath(self.top_rect_path, self.color_top) + painter.strokePath(self.round_rect_path, QPen(Qt.gray, 1)) + + # TODO: Build the pointing arrow? + + def keyReleaseEvent(self, event): + """ """ + key = event.key() + self.key_pressed = key +# print(key) + keys = [Qt.Key_Right, Qt.Key_Left, Qt.Key_Down, Qt.Key_Up, + Qt.Key_Escape, Qt.Key_PageUp, Qt.Key_PageDown, + Qt.Key_Home, Qt.Key_End, Qt.Key_Menu] + + if key in keys: + if not self.is_fade_running(): + self.sig_key_pressed.emit() + + def mousePressEvent(self, event): + """override Qt method""" + # Raise the main application window on click + self.parent.raise_() + self.raise_() + + if event.button() == Qt.RightButton: + pass +# clicked_widget = self.childAt(event.x(), event.y()) +# if clicked_widget == self.label_current: +# self.context_menu_requested(event) + + def context_menu_requested(self, event): + """ """ + pos = QPoint(event.x(), event.y()) + menu = QMenu(self) + + actions = [] + action_title = create_action(self, _('Go to step: '), icon=QIcon()) + action_title.setDisabled(True) + actions.append(action_title) +# actions.append(create_action(self, _(': '), icon=QIcon())) + + add_actions(menu, actions) + + menu.popup(self.mapToGlobal(pos)) + + def reject(self): + """Qt method to handle escape key event""" + if not self.is_fade_running(): + key = Qt.Key_Escape + self.key_pressed = key + self.sig_key_pressed.emit() + + +class AnimatedTour(QWidget): + """ """ + + def __init__(self, parent): + QWidget.__init__(self, parent) + + self.parent = parent + + # Variables to adjust + self.duration_canvas = [666, 666] + self.duration_tips = [333, 333] + self.opacity_canvas = [0.0, 0.7] + self.opacity_tips = [0.0, 1.0] + self.color = Qt.black + self.easing_curve = [QEasingCurve.Linear] + + self.current_step = 0 + self.step_current = 0 + self.steps = 0 + self.canvas = None + self.tips = None + self.frames = None + self.spy_window = None + + self.widgets = None + self.dockwidgets = None + self.decoration = None + self.run = None + + self.is_tour_set = False + + # Widgets + self.canvas = FadingCanvas(self.parent, self.opacity_canvas, + self.duration_canvas, self.easing_curve, + self.color) + self.tips = FadingTipBox(self.parent, self.opacity_tips, + self.duration_tips, self.easing_curve) + + # Widgets setup + # Needed to fix issue #2204 + self.setAttribute(Qt.WA_TransparentForMouseEvents) + + # Signals and slots + self.tips.button_next.clicked.connect(self.next_step) + self.tips.button_previous.clicked.connect(self.previous_step) + self.tips.button_close.clicked.connect(self.close_tour) + self.tips.button_run.clicked.connect(self.run_code) + self.tips.button_home.clicked.connect(self.first_step) + self.tips.button_end.clicked.connect(self.last_step) + self.tips.button_run.clicked.connect( + lambda: self.tips.button_run.setDisabled(True)) + self.tips.combo_title.currentIndexChanged.connect(self.go_to_step) + + # Main window move or resize + self.parent.sig_resized.connect(self._resized) + self.parent.sig_moved.connect(self._moved) + + # To capture the arrow keys that allow moving the tour + self.tips.sig_key_pressed.connect(self._key_pressed) + + def _resized(self, event): + """ """ + size = event.size() + self.canvas.setFixedSize(size) + self.canvas.update_canvas() + + if self.is_tour_set: + self._set_data() + + def _moved(self, event): + """ """ + pos = event.pos() + self.canvas.move(QPoint(pos.x(), pos.y())) + + if self.is_tour_set: + self._set_data() + + def _close_canvas(self): + """ """ + self._set_modal(False, [self.tips]) + self.tips.hide() + self.canvas.fade_out(self.canvas.hide) + + def _clear_canvas(self): + """ """ + # TODO: Add option to also make it white... might be usefull? + # Make canvas black before transitions + self.canvas.update_widgets(None) + self.canvas.update_decoration(None) + self.canvas.update_canvas() + + def _move_step(self): + """ """ + self._set_data() + + # Show/raise the widget so it is located first! + widgets = self.dockwidgets + if widgets is not None: + widget = widgets[0] + if widget is not None: + widget.show() + widget.raise_() + + self._locate_tip_box() + + # Change in canvas only after fadein finishes, for visual aesthetics + self.tips.fade_in(self.canvas.update_canvas) + self.tips.raise_() + + def _set_modal(self, value, widgets): + """ """ + platform = sys.platform.lower() + + if 'linux' in platform: + pass + elif 'win' in platform: + for widget in widgets: + widget.setModal(value) + widget.hide() + widget.show() + elif 'darwin' in platform: + pass + else: + pass + + def _process_widgets(self, names, spy_window): + """ """ + widgets = [] + dockwidgets = [] + + for name in names: + base = name.split('.')[0] + temp = getattr(spy_window, base) + + # Check if it is the current editor + if 'get_current_editor()' in name: + temp = temp.get_current_editor() + temp = getattr(temp, name.split('.')[-1]) + + widgets.append(temp) + + # Check if it is a dockwidget and make the widget a dockwidget + # If not return the same widget + temp = getattr(temp, 'dockwidget', temp) + dockwidgets.append(temp) + + return widgets, dockwidgets + + def _set_data(self): + """ """ + step, steps, frames = self.step_current, self.steps, self.frames + current = '{0}/{1}'.format(step + 1, steps) + frame = frames[step] + + combobox_frames = [u"{0}. {1}".format(i+1, f['title']) + for i, f in enumerate(frames)] + + title, content, image = '', '', None + widgets, dockwidgets, decoration = None, None, None + run = None + + # Check if entry exists in dic and act accordingly + if 'title' in frame: + title = frame['title'] + + if 'content' in frame: + content = frame['content'] + + if 'widgets' in frame: + widget_names = frames[step]['widgets'] + # Get the widgets based on their name + widgets, dockwidgets = self._process_widgets(widget_names, + self.spy_window) + self.widgets = widgets + self.dockwidgets = dockwidgets + + if 'decoration' in frame: + widget_names = frames[step]['decoration'] + deco, decoration = self._process_widgets(widget_names, + self.spy_window) + self.decoration = decoration + + if 'image' in frame: + image = frames[step]['image'] + + if 'interact' in frame: + self.canvas.set_interaction(frame['interact']) + if frame['interact']: + self._set_modal(False, [self.tips]) + else: + self._set_modal(True, [self.tips]) + else: + self.canvas.set_interaction(False) + self._set_modal(True, [self.tips]) + + if 'run' in frame: + # Asume that the frist widget is the console + run = frame['run'] + self.run = run + + self.tips.set_data(title, content, current, image, run, + frames=combobox_frames, step=step) + self._check_buttons() + + # Make canvas black when starting a new place of decoration + self.canvas.update_widgets(dockwidgets) + self.canvas.update_decoration(decoration) + + def _locate_tip_box(self): + """ """ + dockwidgets = self.dockwidgets + + # Store the dimensions of the main window + geo = self.parent.frameGeometry() + x, y, width, height = geo.x(), geo.y(), geo.width(), geo.height() + self.width_main = width + self.height_main = height + self.x_main = x + self.y_main = y + + delta = 20 + + # Here is the tricky part to define the best position for the + # tip widget + if dockwidgets is not None: + if dockwidgets[0] is not None: + geo = dockwidgets[0].geometry() + x, y, width, height = geo.x(), geo.y(), geo.width(), geo.height() + + point = dockwidgets[0].mapToGlobal(QPoint(0, 0)) + x_glob, y_glob = point.x(), point.y() + + # Check if is too tall and put to the side + y_fac = (height / self.height_main) * 100 + + if y_fac > 60: # FIXME: + if x < self.tips.width(): + x = x_glob + width + delta + y = y_glob + height/2 - self.tips.height()/2 + else: + x = x_glob - self.tips.width() - delta + y = y_glob + height/2 - self.tips.height()/2 + else: + if y < self.tips.height(): + x = x_glob + width/2 - self.tips.width()/2 + y = y_glob + height + delta + else: + x = x_glob + width/2 - self.tips.width()/2 + y = y_glob - delta - self.tips.height() + else: + # Center on parent + x = self.x_main + self.width_main/2 - self.tips.width()/2 + y = self.y_main + self.height_main/2 - self.tips.height()/2 + + self.tips.set_pos(x, y) + + def _check_buttons(self): + """ """ + step, steps = self.step_current, self.steps + self.tips.button_disable = None + + if step == 0: + self.tips.button_disable = 'previous' + + if step == steps - 1: + self.tips.button_disable = 'next' + + def _key_pressed(self): + """ """ + key = self.tips.key_pressed + + if ((key == Qt.Key_Right or key == Qt.Key_Down or + key == Qt.Key_PageDown) and self.step_current != self.steps - 1): + self.next_step() + elif ((key == Qt.Key_Left or key == Qt.Key_Up or + key == Qt.Key_PageUp) and self.step_current != 0): + self.previous_step() + elif key == Qt.Key_Escape: + self.close_tour() + elif key == Qt.Key_Home and self.step_current != 0: + self.first_step() + elif key == Qt.Key_End and self.step_current != self.steps - 1: + self.last_step() + elif key == Qt.Key_Menu: + pos = self.tips.label_current.pos() + self.tips.context_menu_requested(pos) + + # --- public api + def run_code(self): + """ """ + codelines = self.run + console = self.widgets[0] + for codeline in codelines: + console.execute_code(codeline) + + def set_tour(self, index, frames, spy_window): + """ """ + self.spy_window = spy_window + self.active_tour_index = index + self.last_frame_active = frames['last'] + self.frames = frames['tour'] + self.steps = len(self.frames) + + self.is_tour_set = True + + def start_tour(self): + """ """ + geo = self.parent.geometry() + x, y, width, height = geo.x(), geo.y(), geo.width(), geo.height() +# self.parent_x = x +# self.parent_y = y +# self.parent_w = width +# self.parent_h = height + + # FIXME: reset step to last used value + # Reset step to begining + self.step_current = self.last_frame_active + + # Adjust the canvas size to match the main window size + self.canvas.setFixedSize(width, height) + self.canvas.move(QPoint(x, y)) + self.canvas.fade_in(self._move_step) + self._clear_canvas() + + def close_tour(self): + """ """ + self.tips.fade_out(self._close_canvas) + self.tips.show() + + try: + # set the last played frame by updating the available tours in + # parent. This info will be lost on restart. + self.parent.tours_available[self.active_tour_index]['last'] =\ + self.step_current + except: + pass + + def next_step(self): + """ """ + self._clear_canvas() + self.step_current += 1 + self.tips.fade_out(self._move_step) + + def previous_step(self): + """ """ + self._clear_canvas() + self.step_current -= 1 + self.tips.fade_out(self._move_step) + + def go_to_step(self, number, id_=None): + """ """ + self._clear_canvas() + self.step_current = number + self.tips.fade_out(self._move_step) + + def last_step(self): + """ """ + self.go_to_step(self.steps - 1) + + def first_step(self): + """ """ + self.go_to_step(0) + +# ---------------------------------------------------------------------------- +# Used for testing the functionality + + +class TestWindow(QMainWindow): + """ """ + sig_resized = Signal("QResizeEvent") + sig_moved = Signal("QMoveEvent") + + def __init__(self): + super(TestWindow, self).__init__() + self.setGeometry(300, 100, 400, 600) + self.setWindowTitle('Exploring QMainWindow') + + self.exit = QAction('Exit', self) + self.exit.setStatusTip('Exit program') + + # create the menu bar + menubar = self.menuBar() + file_ = menubar.addMenu('&File') + file_.addAction(self.exit) + + # create the status bar + self.statusBar() + + # QWidget or its instance needed for box layout + self.widget = QWidget(self) + + self.button = QPushButton('test') + self.button1 = QPushButton('1') + self.button2 = QPushButton('2') + + effect = QGraphicsOpacityEffect(self.button2) + self.button2.setGraphicsEffect(effect) + self.anim = QPropertyAnimation(effect, "opacity") + self.anim.setStartValue(0.01) + self.anim.setEndValue(1.0) + self.anim.setDuration(500) + + lay = QVBoxLayout() + lay.addWidget(self.button) + lay.addStretch() + lay.addWidget(self.button1) + lay.addWidget(self.button2) + + self.widget.setLayout(lay) + + self.setCentralWidget(self.widget) + self.button.clicked.connect(self.action1) + self.button1.clicked.connect(self.action2) + + self.tour = AnimatedTour(self) + + def action1(self): + """ """ + frames = get_tour('test') + index = 0 + dic = {'last': 0, 'tour': frames} + self.tour.set_tour(index, dic, self) + self.tour.start_tour() + + def action2(self): + """ """ + self.anim.start() + + def resizeEvent(self, event): + """Reimplement Qt method""" + QMainWindow.resizeEvent(self, event) + self.sig_resized.emit(event) + + def moveEvent(self, event): + """Reimplement Qt method""" + QMainWindow.moveEvent(self, event) + self.sig_moved.emit(event) + + +def test(): + """ """ + app = QApplication([]) + win = TestWindow() + win.show() + app.exec_() + + +if __name__ == '__main__': + test() diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/baseconfig.py spyder-3.0.2+dfsg1/spyder/baseconfig.py --- spyder-2.3.8+dfsg1/spyder/baseconfig.py 2015-11-27 14:29:54.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/baseconfig.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,303 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright © 2011-2013 Pierre Raybaut -# Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) - -""" -Spyder base configuration management - -As opposed to spyderlib/config.py, this configuration script deals -exclusively with non-GUI features configuration only -(in other words, we won't import any PyQt object here, avoiding any -sip API incompatibility issue in spyderlib's non-gui modules) -""" - -from __future__ import print_function - -import os.path as osp -import os -import sys - -# Local imports -from spyderlib import __version__ -from spyderlib.utils import encoding -from spyderlib.py3compat import (is_unicode, TEXT_TYPES, INT_TYPES, PY3, - to_text_string, is_text_string) - - -#============================================================================== -# Only for development -#============================================================================== -# To activate/deactivate certain things for development -# SPYDER_DEV is (and *only* has to be) set in bootstrap.py -DEV = os.environ.get('SPYDER_DEV') - -# For testing purposes -# SPYDER_TEST can be set using the --test option of bootstrap.py -TEST = os.environ.get('SPYDER_TEST') - - -#============================================================================== -# Debug helpers -#============================================================================== -STDOUT = sys.stdout -STDERR = sys.stderr -def _get_debug_env(): - debug_env = os.environ.get('SPYDER_DEBUG', '') - if not debug_env.isdigit(): - debug_env = bool(debug_env) - return int(debug_env) -DEBUG = _get_debug_env() - -def debug_print(message): - """Output debug messages to stdout""" - if DEBUG: - ss = STDOUT - print(message, file=ss) - -#============================================================================== -# Configuration paths -#============================================================================== -# Spyder settings dir -if TEST is None: - SUBFOLDER = '.spyder%s' % __version__.split('.')[0] -else: - SUBFOLDER = 'spyder_test' - - -# We can't have PY2 and PY3 settings in the same dir because: -# 1. This leads to ugly crashes and freezes (e.g. by trying to -# embed a PY2 interpreter in PY3) -# 2. We need to save the list of installed modules (for code -# completion) separately for each version -if PY3: - SUBFOLDER = SUBFOLDER + '-py3' - - -def get_home_dir(): - """ - Return user home directory - """ - try: - # expanduser() returns a raw byte string which needs to be - # decoded with the codec that the OS is using to represent file paths. - path = encoding.to_unicode_from_fs(osp.expanduser('~')) - except: - path = '' - for env_var in ('HOME', 'USERPROFILE', 'TMP'): - if osp.isdir(path): - break - # os.environ.get() returns a raw byte string which needs to be - # decoded with the codec that the OS is using to represent environment - # variables. - path = encoding.to_unicode_from_fs(os.environ.get(env_var, '')) - if path: - return path - else: - raise RuntimeError('Please define environment variable $HOME') - - -def get_conf_path(filename=None): - """Return absolute path for configuration file with specified filename""" - if TEST is None: - conf_dir = osp.join(get_home_dir(), SUBFOLDER) - else: - import tempfile - conf_dir = osp.join(tempfile.gettempdir(), SUBFOLDER) - if not osp.isdir(conf_dir): - os.mkdir(conf_dir) - if filename is None: - return conf_dir - else: - return osp.join(conf_dir, filename) - - -def get_module_path(modname): - """Return module *modname* base path""" - return osp.abspath(osp.dirname(sys.modules[modname].__file__)) - - -def get_module_data_path(modname, relpath=None, attr_name='DATAPATH'): - """Return module *modname* data path - Note: relpath is ignored if module has an attribute named *attr_name* - - Handles py2exe/cx_Freeze distributions""" - datapath = getattr(sys.modules[modname], attr_name, '') - if datapath: - return datapath - else: - datapath = get_module_path(modname) - parentdir = osp.join(datapath, osp.pardir) - if osp.isfile(parentdir): - # Parent directory is not a directory but the 'library.zip' file: - # this is either a py2exe or a cx_Freeze distribution - datapath = osp.abspath(osp.join(osp.join(parentdir, osp.pardir), - modname)) - if relpath is not None: - datapath = osp.abspath(osp.join(datapath, relpath)) - return datapath - - -def get_module_source_path(modname, basename=None): - """Return module *modname* source path - If *basename* is specified, return *modname.basename* path where - *modname* is a package containing the module *basename* - - *basename* is a filename (not a module name), so it must include the - file extension: .py or .pyw - - Handles py2exe/cx_Freeze distributions""" - srcpath = get_module_path(modname) - parentdir = osp.join(srcpath, osp.pardir) - if osp.isfile(parentdir): - # Parent directory is not a directory but the 'library.zip' file: - # this is either a py2exe or a cx_Freeze distribution - srcpath = osp.abspath(osp.join(osp.join(parentdir, osp.pardir), - modname)) - if basename is not None: - srcpath = osp.abspath(osp.join(srcpath, basename)) - return srcpath - - -def is_py2exe_or_cx_Freeze(): - """Return True if this is a py2exe/cx_Freeze distribution of Spyder""" - return osp.isfile(osp.join(get_module_path('spyderlib'), osp.pardir)) - - -SCIENTIFIC_STARTUP = get_module_source_path('spyderlib', - 'scientific_startup.py') - - -#============================================================================== -# Image path list -#============================================================================== - -IMG_PATH = [] -def add_image_path(path): - if not osp.isdir(path): - return - global IMG_PATH - IMG_PATH.append(path) - for _root, dirs, _files in os.walk(path): - for dir in dirs: - IMG_PATH.append(osp.join(path, dir)) - -add_image_path(get_module_data_path('spyderlib', relpath='images')) - -from spyderlib.otherplugins import PLUGIN_PATH -if PLUGIN_PATH is not None: - add_image_path(osp.join(PLUGIN_PATH, 'images')) - -def get_image_path(name, default="not_found.png"): - """Return image absolute path""" - for img_path in IMG_PATH: - full_path = osp.join(img_path, name) - if osp.isfile(full_path): - return osp.abspath(full_path) - if default is not None: - return osp.abspath(osp.join(img_path, default)) - - -#============================================================================== -# Translations -#============================================================================== -def get_translation(modname, dirname=None): - """Return translation callback for module *modname*""" - if dirname is None: - dirname = modname - locale_path = get_module_data_path(dirname, relpath="locale", - attr_name='LOCALEPATH') - # fixup environment var LANG in case it's unknown - if "LANG" not in os.environ: - import locale - lang = locale.getdefaultlocale()[0] - if lang is not None: - os.environ["LANG"] = lang - import gettext - try: - _trans = gettext.translation(modname, locale_path, codeset="utf-8") - lgettext = _trans.lgettext - def translate_gettext(x): - if not PY3 and is_unicode(x): - x = x.encode("utf-8") - y = lgettext(x) - if is_text_string(y) and PY3: - return y - else: - return to_text_string(y, "utf-8") - return translate_gettext - except IOError as _e: # analysis:ignore - #print "Not using translations (%s)" % _e - def translate_dumb(x): - if not is_unicode(x): - return to_text_string(x, "utf-8") - return x - return translate_dumb - -# Translation callback -_ = get_translation("spyderlib") - - -#============================================================================== -# Namespace Browser (Variable Explorer) configuration management -#============================================================================== - -def get_supported_types(): - """ - Return a dictionnary containing types lists supported by the - namespace browser: - dict(picklable=picklable_types, editable=editables_types) - - See: - get_remote_data function in spyderlib/widgets/externalshell/monitor.py - get_internal_shell_filter method in namespacebrowser.py - - Note: - If you update this list, don't forget to update doc/variablexplorer.rst - """ - from datetime import date - editable_types = [int, float, complex, list, dict, tuple, date - ] + list(TEXT_TYPES) + list(INT_TYPES) - try: - from numpy import ndarray, matrix, generic - editable_types += [ndarray, matrix, generic] - except ImportError: - pass - try: - from pandas import DataFrame, Series - editable_types += [DataFrame, Series] - except ImportError: - pass - picklable_types = editable_types[:] - try: - from spyderlib.pil_patch import Image - editable_types.append(Image.Image) - except ImportError: - pass - return dict(picklable=picklable_types, editable=editable_types) - -# Variable explorer display / check all elements data types for sequences: -# (when saving the variable explorer contents, check_all is True, -# see widgets/externalshell/namespacebrowser.py:NamespaceBrowser.save_data) -CHECK_ALL = False #XXX: If True, this should take too much to compute... - -EXCLUDED_NAMES = ['nan', 'inf', 'infty', 'little_endian', 'colorbar_doc', - 'typecodes', '__builtins__', '__main__', '__doc__', 'NaN', - 'Inf', 'Infinity', 'sctypes', 'rcParams', 'rcParamsDefault', - 'sctypeNA', 'typeNA', 'False_', 'True_',] - -#============================================================================== -# Mac application utilities -#============================================================================== - -if PY3: - MAC_APP_NAME = 'Spyder.app' -else: - MAC_APP_NAME = 'Spyder-Py2.app' - -def running_in_mac_app(): - if sys.platform == "darwin" and MAC_APP_NAME in __file__: - return True - else: - return False diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/cli_options.py spyder-3.0.2+dfsg1/spyder/cli_options.py --- spyder-2.3.8+dfsg1/spyder/cli_options.py 2015-08-24 21:09:44.000000000 +0200 +++ spyder-3.0.2+dfsg1/spyder/cli_options.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,43 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright © 2012 The Spyder development team -# Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) - -import optparse - -def get_options(): - """ - Convert options into commands - return commands, message - """ - parser = optparse.OptionParser(usage="spyder [options] files") - parser.add_option('-l', '--light', action='store_true', default=False, - help="Light version (all add-ons are disabled)") - parser.add_option('--new-instance', action='store_true', default=False, - help="Run a new instance of Spyder, even if the single " - "instance mode has been turned on (default)") - parser.add_option('--session', dest="startup_session", default='', - help="Startup session") - parser.add_option('--defaults', dest="reset_to_defaults", - action='store_true', default=False, - help="Reset configuration settings to defaults") - parser.add_option('--reset', dest="reset_session", - action='store_true', default=False, - help="Remove all configuration files!") - parser.add_option('--optimize', action='store_true', default=False, - help="Optimize Spyder bytecode (this may require " - "administrative privileges)") - parser.add_option('-w', '--workdir', dest="working_directory", default=None, - help="Default working directory") - parser.add_option('--show-console', action='store_true', default=False, - help="Do not hide parent console window (Windows)") - parser.add_option('--multithread', dest="multithreaded", - action='store_true', default=False, - help="Internal console is executed in another thread " - "(separate from main application thread)") - parser.add_option('--profile', action='store_true', default=False, - help="Profile mode (internal test, " - "not related with Python profiling)") - options, args = parser.parse_args() - return options, args diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/config/base.py spyder-3.0.2+dfsg1/spyder/config/base.py --- spyder-2.3.8+dfsg1/spyder/config/base.py 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/config/base.py 2016-11-19 01:31:32.000000000 +0100 @@ -0,0 +1,430 @@ +# -*- coding: utf-8 -*- +# +# Copyright © Spyder Project Contributors +# Licensed under the terms of the MIT License +# (see spyder/__init__.py for details) + +""" +Spyder base configuration management + +This file only deals with non-GUI configuration features +(in other words, we won't import any PyQt object here, avoiding any +sip API incompatibility issue in spyder's non-gui modules) +""" + +from __future__ import print_function + +import codecs +import locale +import os.path as osp +import os +import shutil +import sys + +# Local imports +from spyder.utils import encoding +from spyder.py3compat import (is_unicode, TEXT_TYPES, INT_TYPES, PY3, + to_text_string, is_text_string) + + +#============================================================================== +# Only for development +#============================================================================== +# To activate/deactivate certain things for development +# SPYDER_DEV is (and *only* has to be) set in bootstrap.py +DEV = os.environ.get('SPYDER_DEV') + +# For testing purposes +# SPYDER_TEST can be set using the --test option of bootstrap.py +TEST = os.environ.get('SPYDER_TEST') + + +#============================================================================== +# Debug helpers +#============================================================================== +# This is needed after restarting and using debug_print +STDOUT = sys.stdout if PY3 else codecs.getwriter('utf-8')(sys.stdout) +STDERR = sys.stderr +def _get_debug_env(): + debug_env = os.environ.get('SPYDER_DEBUG', '') + if not debug_env.isdigit(): + debug_env = bool(debug_env) + return int(debug_env) +DEBUG = _get_debug_env() + +def debug_print(*message): + """Output debug messages to stdout""" + if DEBUG: + ss = STDOUT + if PY3: + # This is needed after restarting and using debug_print + for m in message: + ss.buffer.write(str(m).encode('utf-8')) + print('', file=ss) + else: + print(*message, file=ss) + + +#============================================================================== +# Configuration paths +#============================================================================== +# Spyder settings dir +# NOTE: During the 2.x.x series this dir was named .spyder2, but +# since 3.0+ we've reverted back to use .spyder to simplify major +# updates in version (required when we change APIs by Linux +# packagers) +if sys.platform.startswith('linux'): + SUBFOLDER = 'spyder' +else: + SUBFOLDER = '.spyder' + + +# We can't have PY2 and PY3 settings in the same dir because: +# 1. This leads to ugly crashes and freezes (e.g. by trying to +# embed a PY2 interpreter in PY3) +# 2. We need to save the list of installed modules (for code +# completion) separately for each version +if PY3: + SUBFOLDER = SUBFOLDER + '-py3' + + +def get_home_dir(): + """ + Return user home directory + """ + try: + # expanduser() returns a raw byte string which needs to be + # decoded with the codec that the OS is using to represent file paths. + path = encoding.to_unicode_from_fs(osp.expanduser('~')) + except: + path = '' + for env_var in ('HOME', 'USERPROFILE', 'TMP'): + if osp.isdir(path): + break + # os.environ.get() returns a raw byte string which needs to be + # decoded with the codec that the OS is using to represent environment + # variables. + path = encoding.to_unicode_from_fs(os.environ.get(env_var, '')) + if path: + return path + else: + raise RuntimeError('Please define environment variable $HOME') + + +def get_conf_path(filename=None): + """Return absolute path for configuration file with specified filename""" + # This makes us follow the XDG standard to save our settings + # on Linux, as it was requested on Issue 2629 + if sys.platform.startswith('linux'): + xdg_config_home = os.environ.get('XDG_CONFIG_HOME', '') + if not xdg_config_home: + xdg_config_home = osp.join(get_home_dir(), '.config') + if not osp.isdir(xdg_config_home): + os.makedirs(xdg_config_home) + conf_dir = osp.join(xdg_config_home, SUBFOLDER) + else: + conf_dir = osp.join(get_home_dir(), SUBFOLDER) + if not osp.isdir(conf_dir): + os.mkdir(conf_dir) + if filename is None: + return conf_dir + else: + return osp.join(conf_dir, filename) + + +def get_module_path(modname): + """Return module *modname* base path""" + return osp.abspath(osp.dirname(sys.modules[modname].__file__)) + + +def get_module_data_path(modname, relpath=None, attr_name='DATAPATH'): + """Return module *modname* data path + Note: relpath is ignored if module has an attribute named *attr_name* + + Handles py2exe/cx_Freeze distributions""" + datapath = getattr(sys.modules[modname], attr_name, '') + if datapath: + return datapath + else: + datapath = get_module_path(modname) + parentdir = osp.join(datapath, osp.pardir) + if osp.isfile(parentdir): + # Parent directory is not a directory but the 'library.zip' file: + # this is either a py2exe or a cx_Freeze distribution + datapath = osp.abspath(osp.join(osp.join(parentdir, osp.pardir), + modname)) + if relpath is not None: + datapath = osp.abspath(osp.join(datapath, relpath)) + return datapath + + +def get_module_source_path(modname, basename=None): + """Return module *modname* source path + If *basename* is specified, return *modname.basename* path where + *modname* is a package containing the module *basename* + + *basename* is a filename (not a module name), so it must include the + file extension: .py or .pyw + + Handles py2exe/cx_Freeze distributions""" + srcpath = get_module_path(modname) + parentdir = osp.join(srcpath, osp.pardir) + if osp.isfile(parentdir): + # Parent directory is not a directory but the 'library.zip' file: + # this is either a py2exe or a cx_Freeze distribution + srcpath = osp.abspath(osp.join(osp.join(parentdir, osp.pardir), + modname)) + if basename is not None: + srcpath = osp.abspath(osp.join(srcpath, basename)) + return srcpath + + +def is_py2exe_or_cx_Freeze(): + """Return True if this is a py2exe/cx_Freeze distribution of Spyder""" + return osp.isfile(osp.join(get_module_path('spyder'), osp.pardir)) + + +SCIENTIFIC_STARTUP = get_module_source_path('spyder', 'scientific_startup.py') + + +#============================================================================== +# Image path list +#============================================================================== +IMG_PATH = [] +def add_image_path(path): + if not osp.isdir(path): + return + global IMG_PATH + IMG_PATH.append(path) + for dirpath, dirnames, _filenames in os.walk(path): + for dirname in dirnames: + IMG_PATH.append(osp.join(dirpath, dirname)) + +add_image_path(get_module_data_path('spyder', relpath='images')) + +def get_image_path(name, default="not_found.png"): + """Return image absolute path""" + for img_path in IMG_PATH: + full_path = osp.join(img_path, name) + if osp.isfile(full_path): + return osp.abspath(full_path) + if default is not None: + return osp.abspath(osp.join(img_path, default)) + + +#============================================================================== +# Translations +#============================================================================== +LANG_FILE = get_conf_path('langconfig') +DEFAULT_LANGUAGE = 'en' + +# This needs to be updated every time a new language is added to spyder, and is +# used by the Preferences configuration to populate the Language QComboBox +LANGUAGE_CODES = {'en': u'English', + 'fr': u'Français', + 'es': u'Español', + 'pt_BR': u'Português', + 'ru': u'Русский', + 'ja': u'日本語' + } + + +def get_available_translations(): + """ + List available translations for spyder based on the folders found in the + locale folder. This function checks if LANGUAGE_CODES contain the same + information that is found in the 'locale' folder to ensure that when a new + language is added, LANGUAGE_CODES is updated. + """ + locale_path = get_module_data_path("spyder", relpath="locale", + attr_name='LOCALEPATH') + listdir = os.listdir(locale_path) + langs = [d for d in listdir if osp.isdir(osp.join(locale_path, d))] + langs = [DEFAULT_LANGUAGE] + langs + + # Check that there is a language code available in case a new translation + # is added, to ensure LANGUAGE_CODES is updated. + for lang in langs: + if lang not in LANGUAGE_CODES: + error = _('Update LANGUAGE_CODES (inside config/base.py) if a new ' + 'translation has been added to Spyder') + raise Exception(error) + return langs + + +def get_interface_language(): + """ + If Spyder has a translation available for the locale language, it will + return the version provided by Spyder adjusted for language subdifferences, + otherwise it will return DEFAULT_LANGUAGE. + + Example: + 1.) Spyder provides ('en', 'fr', 'es' and 'pt_BR'), if the locale is + either 'en_US' or 'en' or 'en_UK', this function will return 'en' + + 2.) Spyder provides ('en', 'fr', 'es' and 'pt_BR'), if the locale is + either 'pt' or 'pt_BR', this function will return 'pt_BR' + """ + locale_language = locale.getdefaultlocale()[0] + + language = DEFAULT_LANGUAGE + + if locale_language is not None: + spyder_languages = get_available_translations() + for lang in spyder_languages: + if locale_language == lang: + language = locale_language + break + elif locale_language.startswith(lang) or \ + lang.startswith(locale_language): + language = lang + break + + return language + + +def save_lang_conf(value): + """Save language setting to language config file""" + with open(LANG_FILE, 'w') as f: + f.write(value) + + +def load_lang_conf(): + """ + Load language setting from language config file if it exists, otherwise + try to use the local settings if Spyder provides a translation, or + return the default if no translation provided. + """ + if osp.isfile(LANG_FILE): + with open(LANG_FILE, 'r') as f: + lang = f.read() + else: + lang = get_interface_language() + save_lang_conf(lang) + return lang + + +def get_translation(modname, dirname=None): + """Return translation callback for module *modname*""" + if dirname is None: + dirname = modname + locale_path = get_module_data_path(dirname, relpath="locale", + attr_name='LOCALEPATH') + # If LANG is defined in ubuntu, a warning message is displayed, so in unix + # systems we define the LANGUAGE variable. + language = load_lang_conf() + if os.name == 'nt': + os.environ["LANG"] = language # Works on Windows + else: + os.environ["LANGUAGE"] = language # Works on Linux + + import gettext + try: + _trans = gettext.translation(modname, locale_path, codeset="utf-8") + lgettext = _trans.lgettext + def translate_gettext(x): + if not PY3 and is_unicode(x): + x = x.encode("utf-8") + y = lgettext(x) + if is_text_string(y) and PY3: + return y + else: + return to_text_string(y, "utf-8") + return translate_gettext + except IOError as _e: # analysis:ignore + #print "Not using translations (%s)" % _e + def translate_dumb(x): + if not is_unicode(x): + return to_text_string(x, "utf-8") + return x + return translate_dumb + +# Translation callback +_ = get_translation("spyder") + + +#============================================================================== +# Namespace Browser (Variable Explorer) configuration management +#============================================================================== +def get_supported_types(): + """ + Return a dictionnary containing types lists supported by the + namespace browser: + dict(picklable=picklable_types, editable=editables_types) + + See: + get_remote_data function in spyder/widgets/variableexplorer/utils/monitor.py + + Note: + If you update this list, don't forget to update doc/variablexplorer.rst + """ + from datetime import date + editable_types = [int, float, complex, list, dict, tuple, date + ] + list(TEXT_TYPES) + list(INT_TYPES) + try: + from numpy import ndarray, matrix, generic + editable_types += [ndarray, matrix, generic] + except ImportError: + pass + try: + from pandas import DataFrame, Series + editable_types += [DataFrame, Series] + except ImportError: + pass + picklable_types = editable_types[:] + try: + from spyder.pil_patch import Image + editable_types.append(Image.Image) + except ImportError: + pass + return dict(picklable=picklable_types, editable=editable_types) + +# Variable explorer display / check all elements data types for sequences: +# (when saving the variable explorer contents, check_all is True, +# see widgets/variableexplorer/namespacebrowser.py:NamespaceBrowser.save_data) +CHECK_ALL = False #XXX: If True, this should take too much to compute... + +EXCLUDED_NAMES = ['nan', 'inf', 'infty', 'little_endian', 'colorbar_doc', + 'typecodes', '__builtins__', '__main__', '__doc__', 'NaN', + 'Inf', 'Infinity', 'sctypes', 'rcParams', 'rcParamsDefault', + 'sctypeNA', 'typeNA', 'False_', 'True_',] + + +#============================================================================== +# Mac application utilities +#============================================================================== +if PY3: + MAC_APP_NAME = 'Spyder.app' +else: + MAC_APP_NAME = 'Spyder-Py2.app' + +def running_in_mac_app(): + if sys.platform == "darwin" and MAC_APP_NAME in __file__: + return True + else: + return False + + +#============================================================================== +# Reset config files +#============================================================================== +SAVED_CONFIG_FILES = ('help', 'onlinehelp', 'path', 'pylint.results', + 'spyder.ini', 'temp.py', 'temp.spydata', 'template.py', + 'history.py', 'history_internal.py', 'workingdir', + '.projects', '.spyderproject', '.ropeproject', + 'monitor.log', 'monitor_debug.log', 'rope.log', + 'langconfig', 'spyder.lock') + + +def reset_config_files(): + """Remove all config files""" + print("*** Reset Spyder settings to defaults ***", file=STDERR) + for fname in SAVED_CONFIG_FILES: + cfg_fname = get_conf_path(fname) + if osp.isfile(cfg_fname) or osp.islink(cfg_fname): + os.remove(cfg_fname) + elif osp.isdir(cfg_fname): + shutil.rmtree(cfg_fname) + else: + continue + print("removing:", cfg_fname, file=STDERR) diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/config/fonts.py spyder-3.0.2+dfsg1/spyder/config/fonts.py --- spyder-2.3.8+dfsg1/spyder/config/fonts.py 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/config/fonts.py 2016-10-25 02:05:22.000000000 +0200 @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +# +# Copyright © Spyder Project Contributors +# Licensed under the terms of the MIT License +# (see spyder/__init__.py for details) + +""" +Spyder font variables +""" + +import os +import sys + +from spyder.config.utils import is_ubuntu + + +#============================================================================== +# Main fonts +#============================================================================== +# Rich text fonts +SANS_SERIF = ['Sans Serif', 'DejaVu Sans', 'Bitstream Vera Sans', + 'Bitstream Charter', 'Lucida Grande', 'MS Shell Dlg 2', + 'Calibri', 'Verdana', 'Geneva', 'Lucid', 'Arial', + 'Helvetica', 'Avant Garde', 'Times', 'sans-serif'] + +# Plan text fonts +MONOSPACE = ['Monospace', 'DejaVu Sans Mono', 'Consolas', + 'Bitstream Vera Sans Mono', 'Andale Mono', 'Liberation Mono', + 'Courier New', 'Courier', 'monospace', 'Fixed', 'Terminal'] + + +#============================================================================== +# Adjust font size per OS +#============================================================================== +if sys.platform == 'darwin': + MONOSPACE = ['Menlo'] + MONOSPACE + BIG = MEDIUM = SMALL = 12 +elif os.name == 'nt': + BIG = 12 + MEDIUM = 10 + SMALL = 9 +elif is_ubuntu(): + SANS_SERIF = ['Ubuntu'] + SANS_SERIF + MONOSPACE = ['Ubuntu Mono'] + MONOSPACE + BIG = 13 + MEDIUM = SMALL = 11 +else: + BIG = 12 + MEDIUM = SMALL = 9 + +DEFAULT_SMALL_DELTA = SMALL - MEDIUM +DEFAULT_LARGE_DELTA = SMALL - BIG diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/config/gui.py spyder-3.0.2+dfsg1/spyder/config/gui.py --- spyder-2.3.8+dfsg1/spyder/config/gui.py 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/config/gui.py 2016-11-18 17:29:56.000000000 +0100 @@ -0,0 +1,172 @@ +# -*- coding: utf-8 -*- +# +# Copyright © Spyder Project Contributors +# Licensed under the terms of the MIT License +# (see spyder/__init__.py for details) + +""" +Spyder GUI-related configuration management +(for non-GUI configuration, see spyder/config/base.py) + +Important note regarding shortcuts: + For compatibility with QWERTZ keyboards, one must avoid using the following + shortcuts: + Ctrl + Alt + Q, W, F, G, Y, X, C, V, B, N +""" + +# Standard library imports +from collections import namedtuple +import sys + +# Third party imports +from qtpy.QtCore import Qt +from qtpy.QtGui import QFont, QFontDatabase, QKeySequence +from qtpy.QtWidgets import QShortcut + +# Local imports +from spyder.config.main import CONF +from spyder.config.user import NoDefault +from spyder.py3compat import to_text_string +from spyder.utils import syntaxhighlighters as sh + + +# Run cell shortcuts +if sys.platform == 'darwin': + RUN_CELL_SHORTCUT = Qt.META + Qt.Key_Return +else: + RUN_CELL_SHORTCUT = Qt.CTRL + Qt.Key_Return +RUN_CELL_AND_ADVANCE_SHORTCUT = Qt.SHIFT + Qt.Key_Return + + +# To save metadata about widget shortcuts (needed to build our +# preferences page) +Shortcut = namedtuple('Shortcut', 'data') + + +def font_is_installed(font): + """Check if font is installed""" + return [fam for fam in QFontDatabase().families() + if to_text_string(fam)==font] + + +def get_family(families): + """Return the first installed font family in family list""" + if not isinstance(families, list): + families = [ families ] + for family in families: + if font_is_installed(family): + return family + else: + print("Warning: None of the following fonts is installed: %r" % families) + return QFont().family() + + +FONT_CACHE = {} + +def get_font(section='main', option='font', font_size_delta=0): + """Get console font properties depending on OS and user options""" + font = FONT_CACHE.get((section, option)) + + if font is None: + families = CONF.get(section, option+"/family", None) + + if families is None: + return QFont() + + family = get_family(families) + weight = QFont.Normal + italic = CONF.get(section, option+'/italic', False) + + if CONF.get(section, option+'/bold', False): + weight = QFont.Bold + + size = CONF.get(section, option+'/size', 9) + font_size_delta + font = QFont(family, size, weight) + font.setItalic(italic) + FONT_CACHE[(section, option)] = font + + size = CONF.get(section, option+'/size', 9) + font_size_delta + font.setPointSize(size) + return font + + +def set_font(font, section='main', option='font'): + """Set font""" + CONF.set(section, option+'/family', to_text_string(font.family())) + CONF.set(section, option+'/size', float(font.pointSize())) + CONF.set(section, option+'/italic', int(font.italic())) + CONF.set(section, option+'/bold', int(font.bold())) + FONT_CACHE[(section, option)] = font + + +def get_shortcut(context, name): + """Get keyboard shortcut (key sequence string)""" + return CONF.get('shortcuts', '%s/%s' % (context, name)) + + +def set_shortcut(context, name, keystr): + """Set keyboard shortcut (key sequence string)""" + CONF.set('shortcuts', '%s/%s' % (context, name), keystr) + + +def fixed_shortcut(keystr, parent, action): + """Define a fixed shortcut according to a keysequence string""" + sc = QShortcut(QKeySequence(keystr), parent, action) + sc.setContext(Qt.WidgetWithChildrenShortcut) + return sc + + +def config_shortcut(action, context, name, parent): + """ + Create a Shortcut namedtuple for a widget + + The data contained in this tuple will be registered in + our shortcuts preferences page + """ + keystr = get_shortcut(context, name) + qsc = fixed_shortcut(keystr, parent, action) + sc = Shortcut(data=(qsc, context, name)) + return sc + + +def iter_shortcuts(): + """Iterate over keyboard shortcuts""" + for option in CONF.options('shortcuts'): + context, name = option.split("/", 1) + yield context, name, get_shortcut(context, name) + + +def reset_shortcuts(): + """Reset keyboard shortcuts to default values""" + CONF.reset_to_defaults(section='shortcuts') + + +def get_color_scheme(name): + """Get syntax color scheme""" + color_scheme = {} + for key in sh.COLOR_SCHEME_KEYS: + color_scheme[key] = CONF.get("color_schemes", "%s/%s" % (name, key)) + return color_scheme + + +def set_color_scheme(name, color_scheme, replace=True): + """Set syntax color scheme""" + section = "color_schemes" + names = CONF.get("color_schemes", "names", []) + for key in sh.COLOR_SCHEME_KEYS: + option = "%s/%s" % (name, key) + value = CONF.get(section, option, default=None) + if value is None or replace or name not in names: + CONF.set(section, option, color_scheme[key]) + names.append(to_text_string(name)) + CONF.set(section, "names", sorted(list(set(names)))) + + +def set_default_color_scheme(name, replace=True): + """Reset color scheme to default values""" + assert name in sh.COLOR_SCHEME_NAMES + set_color_scheme(name, sh.get_color_scheme(name), replace=replace) + + +for _name in sh.COLOR_SCHEME_NAMES: + set_default_color_scheme(_name, replace=False) diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/config/ipython.py spyder-3.0.2+dfsg1/spyder/config/ipython.py --- spyder-2.3.8+dfsg1/spyder/config/ipython.py 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/config/ipython.py 2016-10-25 02:05:22.000000000 +0200 @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# +# Copyright © Spyder Project Contributors +# Licensed under the terms of the MIT License +# (see spyder/__init__.py for details) + +""" +IPython configuration variables needed by Spyder +""" + +from spyder.utils import programs +from spyder import dependencies +from spyder.config.base import _ + + +# Constants +QTCONSOLE_REQVER = ">=4.2.0" +ZMQ_REQVER = ">=13.0.0" +NBCONVERT_REQVER = ">=4.0" + + +# Dependencies +dependencies.add("qtconsole", _("Integrate the IPython console"), + required_version=QTCONSOLE_REQVER) +dependencies.add("nbconvert", _("Manipulate Jupyter notebooks on the Editor"), + required_version=NBCONVERT_REQVER) + + +# Auxiliary functions +def is_qtconsole_installed(): + pyzmq_installed = programs.is_module_installed('zmq', version=ZMQ_REQVER) + pygments_installed = programs.is_module_installed('pygments') + qtconsole_installed = programs.is_module_installed('qtconsole', + version=QTCONSOLE_REQVER) + + if pyzmq_installed and pygments_installed and qtconsole_installed: + return True + else: + return False + + +# Main check for IPython presence +QTCONSOLE_INSTALLED = is_qtconsole_installed() diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/config/main.py spyder-3.0.2+dfsg1/spyder/config/main.py --- spyder-2.3.8+dfsg1/spyder/config/main.py 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/config/main.py 2016-11-17 04:39:39.000000000 +0100 @@ -0,0 +1,608 @@ +# -*- coding: utf-8 -*- +# +# Copyright © Spyder Project Contributors +# Licensed under the terms of the MIT License +# (see spyder/__init__.py for details) + +""" +Spyder configuration options + +Note: Leave this file free of Qt related imports, so that it can be used to +quickly load a user config file +""" + +import os +import sys +import os.path as osp + +# Local import +from spyder.config.base import (CHECK_ALL, EXCLUDED_NAMES, get_home_dir, + SUBFOLDER, TEST) +from spyder.config.fonts import BIG, MEDIUM, MONOSPACE, SANS_SERIF +from spyder.config.user import UserConfig +from spyder.config.utils import IMPORT_EXT +from spyder.utils import codeanalysis + + +#============================================================================== +# Main constants +#============================================================================== +# Find in files exclude patterns +EXCLUDE_PATTERNS = [r'\.pyc$|\.pyo$|\.orig$|\.hg|\.svn|\bbuild\b', + r'\.pyc$|\.pyo$|\.orig$|\.hg|\.svn'] + +# Extensions that should be visible in Spyder's file/project explorers +SHOW_EXT = ['.py', '.ipynb', '.txt', '.dat', '.pdf', '.png', '.svg'] + + +# Extensions supported by Spyder (Editor or Variable explorer) +USEFUL_EXT = IMPORT_EXT + SHOW_EXT + + +# Name filters for file/project explorers (excluding files without extension) +NAME_FILTERS = ['README', 'INSTALL', 'LICENSE', 'CHANGELOG'] + \ + ['*' + _ext for _ext in USEFUL_EXT if _ext] + + +# Port used to detect if there is a running instance and to communicate with +# it to open external files +OPEN_FILES_PORT = 21128 + + +# OS Specific +WIN = os.name == 'nt' +MAC = sys.platform == 'darwin' +CTRL = "Meta" if MAC else "Ctrl" + + +# ============================================================================= +# Defaults +# ============================================================================= +DEFAULTS = [ + ('main', + { + 'icon_theme': 'spyder 3', + 'single_instance': True, + 'open_files_port': OPEN_FILES_PORT, + 'tear_off_menus': False, + 'high_dpi_scaling': False, + 'vertical_dockwidget_titlebars': False, + 'vertical_tabs': False, + 'animated_docks': True, + 'prompt_on_exit': False, + 'panes_locked': True, + 'window/size': (1260, 740), + 'window/position': (10, 10), + 'window/is_maximized': True, + 'window/is_fullscreen': False, + 'window/prefs_dialog_size': (745, 411), + 'show_status_bar': True, + 'memory_usage/enable': True, + 'memory_usage/timeout': 2000, + 'cpu_usage/enable': False, + 'cpu_usage/timeout': 2000, + 'use_custom_margin': True, + 'custom_margin': 0, + 'show_internal_console_if_traceback': True, + 'check_updates_on_startup': True, + 'toolbars_visible': True, + # Global Spyder fonts + 'font/family': MONOSPACE, + 'font/size': MEDIUM, + 'font/italic': False, + 'font/bold': False, + 'rich_font/family': SANS_SERIF, + 'rich_font/size': BIG, + 'rich_font/italic': False, + 'rich_font/bold': False, + 'cursor/width': 2, + 'completion/size': (300, 180), + }), + ('quick_layouts', + { + 'place_holder': '', + 'names': ['Matlab layout', 'Rstudio layout', 'Vertical split', 'Horizontal split'], + 'order': ['Matlab layout', 'Rstudio layout', 'Vertical split', 'Horizontal split'], + 'active': ['Matlab layout', 'Rstudio layout', 'Vertical split', 'Horizontal split'], + }), + ('internal_console', + { + 'max_line_count': 300, + 'working_dir_history': 30, + 'working_dir_adjusttocontents': False, + 'wrap': True, + 'calltips': True, + 'codecompletion/auto': False, + 'codecompletion/enter_key': True, + 'codecompletion/case_sensitive': True, + 'external_editor/path': 'SciTE', + 'external_editor/gotoline': '-goto:', + 'light_background': True, + }), + ('main_interpreter', + { + 'default': True, + 'custom': False, + 'umr/enabled': True, + 'umr/verbose': True, + 'umr/namelist': [], + }), + ('console', + { + 'max_line_count': 500, + 'wrap': True, + 'single_tab': True, + 'calltips': True, + 'codecompletion/auto': True, + 'codecompletion/enter_key': True, + 'codecompletion/case_sensitive': True, + 'show_elapsed_time': False, + 'show_icontext': False, + 'monitor/enabled': True, + 'qt/api': 'default', + 'matplotlib/backend/value': 0, + 'light_background': True, + 'merge_output_channels': os.name != 'nt', + 'colorize_sys_stderr': os.name != 'nt', + 'pythonstartup/default': True, + 'pythonstartup/custom': False, + 'ets_backend': 'qt4' + }), + ('ipython_console', + { + 'show_banner': True, + 'completion_type': 0, + 'use_pager': False, + 'show_calltips': True, + 'ask_before_closing': False, + 'buffer_size': 500, + 'pylab': True, + 'pylab/autoload': False, + 'pylab/backend': 0, + 'pylab/inline/figure_format': 0, + 'pylab/inline/resolution': 72, + 'pylab/inline/width': 6, + 'pylab/inline/height': 4, + 'startup/run_lines': '', + 'startup/use_run_file': False, + 'startup/run_file': '', + 'greedy_completer': False, + 'autocall': 0, + 'symbolic_math': False, + 'in_prompt': '', + 'out_prompt': '', + 'light_color': True, + 'dark_color': False + }), + ('variable_explorer', + { + 'autorefresh': False, + 'autorefresh/timeout': 2000, + 'check_all': CHECK_ALL, + 'excluded_names': EXCLUDED_NAMES, + 'exclude_private': True, + 'exclude_uppercase': True, + 'exclude_capitalized': False, + 'exclude_unsupported': True, + 'truncate': True, + 'minmax': False, + 'remote_editing': False, + }), + ('editor', + { + 'printer_header/font/family': SANS_SERIF, + 'printer_header/font/size': MEDIUM, + 'printer_header/font/italic': False, + 'printer_header/font/bold': False, + 'wrap': False, + 'wrapflag': True, + 'code_analysis/pyflakes': True, + 'code_analysis/pep8': False, + 'todo_list': True, + 'realtime_analysis': True, + 'realtime_analysis/timeout': 2500, + 'outline_explorer': True, + 'line_numbers': True, + 'blank_spaces': False, + 'edge_line': True, + 'edge_line_column': 79, + 'toolbox_panel': True, + 'calltips': True, + 'go_to_definition': True, + 'close_parentheses': True, + 'close_quotes': False, + 'add_colons': True, + 'auto_unindent': True, + 'indent_chars': '* *', + 'tab_stop_width': 40, + 'codecompletion/auto': True, + 'codecompletion/enter_key': True, + 'codecompletion/case_sensitive': True, + 'check_eol_chars': True, + 'tab_always_indent': False, + 'intelligent_backspace': True, + 'highlight_current_line': True, + 'highlight_current_cell': True, + 'occurrence_highlighting': True, + 'occurrence_highlighting/timeout': 1500, + 'always_remove_trailing_spaces': False, + 'fullpath_sorting': True, + 'show_tab_bar': True, + 'max_recent_files': 20, + 'save_all_before_run': True, + 'focus_to_editor': True, + 'onsave_analysis': False + }), + ('historylog', + { + 'enable': True, + 'max_entries': 100, + 'wrap': True, + 'go_to_eof': True, + }), + ('help', + { + 'enable': True, + 'max_history_entries': 20, + 'wrap': True, + 'connect/editor': False, + 'connect/python_console': False, + 'connect/ipython_console': False, + 'math': True, + 'automatic_import': True, + }), + ('onlinehelp', + { + 'enable': True, + 'zoom_factor': .8, + 'max_history_entries': 20, + }), + ('outline_explorer', + { + 'enable': True, + 'show_fullpath': False, + 'show_all_files': False, + 'show_comments': True, + }), + ('project_explorer', + { + 'name_filters': NAME_FILTERS, + 'show_all': True, + 'show_hscrollbar': True + }), + ('explorer', + { + 'enable': True, + 'wrap': True, + 'name_filters': NAME_FILTERS, + 'show_hidden': True, + 'show_all': True, + 'show_icontext': False, + }), + ('find_in_files', + { + 'enable': True, + 'supported_encodings': ["utf-8", "iso-8859-1", "cp1252"], + 'include': '', + 'include_regexp': True, + 'exclude': EXCLUDE_PATTERNS, + 'exclude_regexp': True, + 'search_text_regexp': True, + 'search_text': [''], + 'search_text_samples': [codeanalysis.TASKS_PATTERN], + 'in_python_path': False, + 'more_options': False, + }), + ('workingdir', + { + 'editor/open/browse_scriptdir': True, + 'editor/open/browse_workdir': False, + 'editor/new/browse_scriptdir': False, + 'editor/new/browse_workdir': True, + 'editor/open/auto_set_to_basedir': False, + 'editor/save/auto_set_to_basedir': False, + 'working_dir_adjusttocontents': False, + 'working_dir_history': 20, + 'startup/use_last_directory': True, + }), + ('shortcuts', + { + # ---- Global ---- + # -- In app/spyder.py + '_/close pane': "Shift+Ctrl+F4", + '_/lock unlock panes': "Shift+Ctrl+F5", + '_/use next layout': "Shift+Alt+PgDown", + '_/use previous layout': "Shift+Alt+PgUp", + '_/preferences': "Ctrl+Alt+Shift+P", + '_/maximize pane': "Ctrl+Alt+Shift+M", + '_/fullscreen mode': "F11", + '_/save current layout': "Shift+Alt+S", + '_/layout preferences': "Shift+Alt+P", + '_/show toolbars': "Alt+Shift+T", + '_/restart': "Shift+Alt+R", + '_/quit': "Ctrl+Q", + # -- In plugins/editor + '_/file switcher': 'Ctrl+P', + '_/debug': "Ctrl+F5", + '_/debug step over': "Ctrl+F10", + '_/debug continue': "Ctrl+F12", + '_/debug step into': "Ctrl+F11", + '_/debug step return': "Ctrl+Shift+F11", + '_/debug exit': "Ctrl+Shift+F12", + '_/run': "F5", + '_/configure': "Ctrl+F6", + '_/re-run last script': "F6", + # -- In plugins/init + '_/switch to help': "Ctrl+Shift+H", + '_/switch to outline_explorer': "Ctrl+Shift+O", + '_/switch to editor': "Ctrl+Shift+E", + '_/switch to historylog': "Ctrl+Shift+L", + '_/switch to onlinehelp': "Ctrl+Shift+D", + '_/switch to project_explorer': "Ctrl+Shift+P", + '_/switch to console': "Ctrl+Shift+C", + '_/switch to ipython_console': "Ctrl+Shift+I", + '_/switch to variable_explorer': "Ctrl+Shift+V", + '_/switch to find_in_files': "Ctrl+Shift+F", + '_/switch to explorer': "Ctrl+Shift+X", + # -- In widgets/findreplace.py + '_/find text': "Ctrl+F", + '_/find next': "F3", + '_/find previous': "Shift+F3", + '_/replace text': "Ctrl+R", + # ---- Editor ---- + # -- In widgets/sourcecode/codeeditor.py + 'editor/code completion': CTRL+'+Space', + 'editor/duplicate line': "Ctrl+Alt+Up" if WIN else \ + "Shift+Alt+Up", + 'editor/copy line': "Ctrl+Alt+Down" if WIN else \ + "Shift+Alt+Down", + 'editor/delete line': 'Ctrl+D', + 'editor/transform to uppercase': 'Ctrl+Shift+U', + 'editor/transform to lowercase': 'Ctrl+U', + 'editor/move line up': "Alt+Up", + 'editor/move line down': "Alt+Down", + 'editor/go to definition': "Ctrl+G", + 'editor/toggle comment': "Ctrl+1", + 'editor/blockcomment': "Ctrl+4", + 'editor/unblockcomment': "Ctrl+5", + 'editor/start of line': "Meta+A", + 'editor/end of line': "Meta+E", + 'editor/previous line': "Meta+P", + 'editor/next line': "Meta+N", + 'editor/previous char': "Meta+B", + 'editor/next char': "Meta+F", + 'editor/previous word': "Meta+Left", + 'editor/next word': "Meta+Right", + 'editor/kill to line end': "Meta+K", + 'editor/kill to line start': "Meta+U", + 'editor/yank': 'Meta+Y', + 'editor/rotate kill ring': 'Shift+Meta+Y', + 'editor/kill previous word': 'Meta+Backspace', + 'editor/kill next word': 'Meta+D', + 'editor/start of document': 'Ctrl+Up', + 'editor/end of document': 'Ctrl+Down', + 'editor/undo': 'Ctrl+Z', + 'editor/redo': 'Ctrl+Shift+Z', + 'editor/cut': 'Ctrl+X', + 'editor/copy': 'Ctrl+C', + 'editor/paste': 'Ctrl+V', + 'editor/delete': 'Delete', + 'editor/select all': "Ctrl+A", + # -- In widgets/editor.py + 'editor/inspect current object': 'Ctrl+I', + 'editor/breakpoint': 'F12', + 'editor/conditional breakpoint': 'Shift+F12', + 'editor/run selection': "F9", + 'editor/go to line': 'Ctrl+L', + 'editor/go to previous file': 'Ctrl+Tab', + 'editor/go to next file': 'Ctrl+Shift+Tab', + 'editor/new file': "Ctrl+N", + 'editor/open file': "Ctrl+O", + 'editor/save file': "Ctrl+S", + 'editor/save all': "Ctrl+Alt+S", + 'editor/save as': 'Ctrl+Shift+S', + 'editor/close all': "Ctrl+Shift+W", + 'editor/last edit location': "Ctrl+Alt+Shift+Left", + 'editor/previous cursor position': "Ctrl+Alt+Left", + 'editor/next cursor position': "Ctrl+Alt+Right", + # -- In plugins/editor.py + 'editor/show/hide outline': "Ctrl+Alt+O", + 'editor/show/hide project explorer': "Ctrl+Alt+P", + # -- In Breakpoints + '_/switch to breakpoints': "Ctrl+Shift+B", + # ---- Consoles (in widgets/shell) ---- + 'console/inspect current object': "Ctrl+I", + 'console/clear shell': "Ctrl+L", + 'console/clear line': "Shift+Escape", + # ---- In Pylint ---- + 'pylint/run analysis': "F8", + # ---- In Profiler ---- + 'profiler/run profiler': "F10" + }), + ('color_schemes', + { + 'names': ['emacs', 'idle', 'monokai', 'pydev', 'scintilla', + 'spyder', 'spyder/dark', 'zenburn'], + 'selected': 'spyder', + # ---- Emacs ---- + 'emacs/name': "Emacs", + # Name Color Bold Italic + 'emacs/background': "#000000", + 'emacs/currentline': "#2b2b43", + 'emacs/currentcell': "#1c1c2d", + 'emacs/occurrence': "#abab67", + 'emacs/ctrlclick': "#0000ff", + 'emacs/sideareas': "#555555", + 'emacs/matched_p': "#009800", + 'emacs/unmatched_p': "#c80000", + 'emacs/normal': ('#ffffff', False, False), + 'emacs/keyword': ('#3c51e8', False, False), + 'emacs/builtin': ('#900090', False, False), + 'emacs/definition': ('#ff8040', True, False), + 'emacs/comment': ('#005100', False, False), + 'emacs/string': ('#00aa00', False, True), + 'emacs/number': ('#800000', False, False), + 'emacs/instance': ('#ffffff', False, True), + # ---- IDLE ---- + 'idle/name': "IDLE", + # Name Color Bold Italic + 'idle/background': "#ffffff", + 'idle/currentline': "#f2e6f3", + 'idle/currentcell': "#feefff", + 'idle/occurrence': "#e8f2fe", + 'idle/ctrlclick': "#0000ff", + 'idle/sideareas': "#efefef", + 'idle/matched_p': "#99ff99", + 'idle/unmatched_p': "#ff9999", + 'idle/normal': ('#000000', False, False), + 'idle/keyword': ('#ff7700', True, False), + 'idle/builtin': ('#900090', False, False), + 'idle/definition': ('#0000ff', False, False), + 'idle/comment': ('#dd0000', False, True), + 'idle/string': ('#00aa00', False, False), + 'idle/number': ('#924900', False, False), + 'idle/instance': ('#777777', True, True), + # ---- Monokai ---- + 'monokai/name': "Monokai", + # Name Color Bold Italic + 'monokai/background': "#2a2b24", + 'monokai/currentline': "#484848", + 'monokai/currentcell': "#3d3d3d", + 'monokai/occurrence': "#666666", + 'monokai/ctrlclick': "#0000ff", + 'monokai/sideareas': "#2a2b24", + 'monokai/matched_p': "#688060", + 'monokai/unmatched_p': "#bd6e76", + 'monokai/normal': ("#ddddda", False, False), + 'monokai/keyword': ("#f92672", False, False), + 'monokai/builtin': ("#ae81ff", False, False), + 'monokai/definition': ("#a6e22e", False, False), + 'monokai/comment': ("#75715e", False, True), + 'monokai/string': ("#e6db74", False, False), + 'monokai/number': ("#ae81ff", False, False), + 'monokai/instance': ("#ddddda", False, True), + # ---- Pydev ---- + 'pydev/name': "Pydev", + # Name Color Bold Italic + 'pydev/background': "#ffffff", + 'pydev/currentline': "#e8f2fe", + 'pydev/currentcell': "#eff8fe", + 'pydev/occurrence': "#ffff99", + 'pydev/ctrlclick': "#0000ff", + 'pydev/sideareas': "#efefef", + 'pydev/matched_p': "#99ff99", + 'pydev/unmatched_p': "#ff99992", + 'pydev/normal': ('#000000', False, False), + 'pydev/keyword': ('#0000ff', False, False), + 'pydev/builtin': ('#900090', False, False), + 'pydev/definition': ('#000000', True, False), + 'pydev/comment': ('#c0c0c0', False, False), + 'pydev/string': ('#00aa00', False, True), + 'pydev/number': ('#800000', False, False), + 'pydev/instance': ('#000000', False, True), + # ---- Scintilla ---- + 'scintilla/name': "Scintilla", + # Name Color Bold Italic + 'scintilla/background': "#ffffff", + 'scintilla/currentline': "#e1f0d1", + 'scintilla/currentcell': "#edfcdc", + 'scintilla/occurrence': "#ffff99", + 'scintilla/ctrlclick': "#0000ff", + 'scintilla/sideareas': "#efefef", + 'scintilla/matched_p': "#99ff99", + 'scintilla/unmatched_p': "#ff9999", + 'scintilla/normal': ('#000000', False, False), + 'scintilla/keyword': ('#00007f', True, False), + 'scintilla/builtin': ('#000000', False, False), + 'scintilla/definition': ('#007f7f', True, False), + 'scintilla/comment': ('#007f00', False, False), + 'scintilla/string': ('#7f007f', False, False), + 'scintilla/number': ('#007f7f', False, False), + 'scintilla/instance': ('#000000', False, True), + # ---- Spyder ---- + 'spyder/name': "Spyder", + # Name Color Bold Italic + 'spyder/background': "#ffffff", + 'spyder/currentline': "#f7ecf8", + 'spyder/currentcell': "#fdfdde", + 'spyder/occurrence': "#ffff99", + 'spyder/ctrlclick': "#0000ff", + 'spyder/sideareas': "#efefef", + 'spyder/matched_p': "#99ff99", + 'spyder/unmatched_p': "#ff9999", + 'spyder/normal': ('#000000', False, False), + 'spyder/keyword': ('#0000ff', False, False), + 'spyder/builtin': ('#900090', False, False), + 'spyder/definition': ('#000000', True, False), + 'spyder/comment': ('#adadad', False, True), + 'spyder/string': ('#00aa00', False, False), + 'spyder/number': ('#800000', False, False), + 'spyder/instance': ('#924900', False, True), + # ---- Spyder/Dark ---- + 'spyder/dark/name': "Spyder Dark", + # Name Color Bold Italic + 'spyder/dark/background': "#131926", + 'spyder/dark/currentline': "#2b2b43", + 'spyder/dark/currentcell': "#31314e", + 'spyder/dark/occurrence': "#abab67", + 'spyder/dark/ctrlclick': "#0000ff", + 'spyder/dark/sideareas': "#282828", + 'spyder/dark/matched_p': "#009800", + 'spyder/dark/unmatched_p': "#c80000", + 'spyder/dark/normal': ('#ffffff', False, False), + 'spyder/dark/keyword': ('#558eff', False, False), + 'spyder/dark/builtin': ('#aa00aa', False, False), + 'spyder/dark/definition': ('#ffffff', True, False), + 'spyder/dark/comment': ('#7f7f7f', False, False), + 'spyder/dark/string': ('#11a642', False, True), + 'spyder/dark/number': ('#c80000', False, False), + 'spyder/dark/instance': ('#be5f00', False, True), + # ---- Zenburn ---- + 'zenburn/name': "Zenburn", + # Name Color Bold Italic + 'zenburn/background': "#3f3f3f", + 'zenburn/currentline': "#333333", + 'zenburn/currentcell': "#2c2c2c", + 'zenburn/occurrence': "#7a738f", + 'zenburn/ctrlclick': "#0000ff", + 'zenburn/sideareas': "#3f3f3f", + 'zenburn/matched_p': "#688060", + 'zenburn/unmatched_p': "#bd6e76", + 'zenburn/normal': ('#dcdccc', False, False), + 'zenburn/keyword': ('#dfaf8f', True, False), + 'zenburn/builtin': ('#efef8f', False, False), + 'zenburn/definition': ('#efef8f', False, False), + 'zenburn/comment': ('#7f9f7f', False, True), + 'zenburn/string': ('#cc9393', False, False), + 'zenburn/number': ('#8cd0d3', False, False), + 'zenburn/instance': ('#dcdccc', False, True) + }) + ] + + +#============================================================================== +# Config instance +#============================================================================== +# IMPORTANT NOTES: +# 1. If you want to *change* the default value of a current option, you need to +# do a MINOR update in config version, e.g. from 3.0.0 to 3.1.0 +# 2. If you want to *remove* options that are no longer needed in our codebase, +# or if you want to *rename* options, then you need to do a MAJOR update in +# version, e.g. from 3.0.0 to 4.0.0 +# 3. You don't need to touch this value if you're just adding a new option +CONF_VERSION = '29.0.0' + +# Main configuration instance +try: + CONF = UserConfig('spyder', defaults=DEFAULTS, load=(not TEST), + version=CONF_VERSION, subfolder=SUBFOLDER, backup=True, + raw_mode=True) +except: + CONF = UserConfig('spyder', defaults=DEFAULTS, load=False, + version=CONF_VERSION, subfolder=SUBFOLDER, backup=True, + raw_mode=True) + +# Removing old .spyder.ini location: +old_location = osp.join(get_home_dir(), '.spyder.ini') +if osp.isfile(old_location): + os.remove(old_location) diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/config/user.py spyder-3.0.2+dfsg1/spyder/config/user.py --- spyder-2.3.8+dfsg1/spyder/config/user.py 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/config/user.py 2016-10-25 02:05:22.000000000 +0200 @@ -0,0 +1,445 @@ +# -*- coding: utf-8 -*- +# +# Copyright © Spyder Project Contributors +# Licensed under the terms of the MIT License +# (see spyder/__init__.py for details) + +""" +This module provides user configuration file management features for Spyder + +It's based on the ConfigParser module (present in the standard library). +""" + +from __future__ import print_function + +# Std imports +import ast +import os +import re +import os.path as osp +import shutil +import time + +# Local imports +from spyder.config.base import (get_conf_path, get_home_dir, + get_module_source_path, TEST) +from spyder.utils.programs import check_version +from spyder.py3compat import configparser as cp +from spyder.py3compat import PY2, is_text_string, to_text_string + +# Std imports for Python 2 +if PY2: + import codecs + + +#============================================================================== +# Auxiliary classes +#============================================================================== +class NoDefault: + pass + + +#============================================================================== +# Defaults class +#============================================================================== +class DefaultsConfig(cp.ConfigParser): + """ + Class used to save defaults to a file and as base class for + UserConfig + """ + def __init__(self, name, subfolder): + cp.ConfigParser.__init__(self) + self.name = name + self.subfolder = subfolder + + def _write(self, fp): + """ + Private write method for Python 2 + The one from configparser fails for non-ascii Windows accounts + """ + if self._defaults: + fp.write("[%s]\n" % cp.DEFAULTSECT) + for (key, value) in self._defaults.items(): + fp.write("%s = %s\n" % (key, str(value).replace('\n', '\n\t'))) + fp.write("\n") + for section in self._sections: + fp.write("[%s]\n" % section) + for (key, value) in self._sections[section].items(): + if key == "__name__": + continue + if (value is not None) or (self._optcre == self.OPTCRE): + value = to_text_string(value) + key = " = ".join((key, value.replace('\n', '\n\t'))) + fp.write("%s\n" % (key)) + fp.write("\n") + + def _set(self, section, option, value, verbose): + """ + Private set method + """ + if not self.has_section(section): + self.add_section( section ) + if not is_text_string(value): + value = repr( value ) + if verbose: + print('%s[ %s ] = %s' % (section, option, value)) + cp.ConfigParser.set(self, section, option, value) + + def _save(self): + """ + Save config into the associated .ini file + """ + # Don't save settings if we are on testing mode + if TEST: + return + + # See Issue 1086 and 1242 for background on why this + # method contains all the exception handling. + fname = self.filename() + + def _write_file(fname): + if PY2: + # Python 2 + with codecs.open(fname, 'w', encoding='utf-8') as configfile: + self._write(configfile) + else: + # Python 3 + with open(fname, 'w', encoding='utf-8') as configfile: + self.write(configfile) + + try: # the "easy" way + _write_file(fname) + except IOError: + try: # the "delete and sleep" way + if osp.isfile(fname): + os.remove(fname) + time.sleep(0.05) + _write_file(fname) + except Exception as e: + print("Failed to write user configuration file.") + print("Please submit a bug report.") + raise(e) + + def filename(self): + """Defines the name of the configuration file to use.""" + # Needs to be done this way to be used by the project config. + # To fix on a later PR + self._filename = getattr(self, '_filename', None) + self._root_path = getattr(self, '_root_path', None) + + if self._filename is None and self._root_path is None: + return self._filename_global() + else: + return self._filename_projects() + + def _filename_projects(self): + """Create a .ini filename located in the current project directory. + This .ini files stores the specific project preferences for each + project created with spyder. + """ + return osp.join(self._root_path, self._filename) + + def _filename_global(self): + """Create a .ini filename located in user home directory. + This .ini files stores the global spyder preferences. + """ + if self.subfolder is None: + config_file = osp.join(get_home_dir(), '.%s.ini' % self.name) + return config_file + else: + folder = get_conf_path() + # Save defaults in a "defaults" dir of .spyder2 to not pollute it + if 'defaults' in self.name: + folder = osp.join(folder, 'defaults') + if not osp.isdir(folder): + os.mkdir(folder) + config_file = osp.join(folder, '%s.ini' % self.name) + return config_file + + def set_defaults(self, defaults): + for section, options in defaults: + for option in options: + new_value = options[ option ] + self._set(section, option, new_value, False) + + +#============================================================================== +# User config class +#============================================================================== +class UserConfig(DefaultsConfig): + """ + UserConfig class, based on ConfigParser + name: name of the config + defaults: dictionnary containing options + *or* list of tuples (section_name, options) + version: version of the configuration file (X.Y.Z format) + subfolder: configuration file will be saved in %home%/subfolder/%name%.ini + + Note that 'get' and 'set' arguments number and type + differ from the overriden methods + """ + DEFAULT_SECTION_NAME = 'main' + def __init__(self, name, defaults=None, load=True, version=None, + subfolder=None, backup=False, raw_mode=False, + remove_obsolete=False): + DefaultsConfig.__init__(self, name, subfolder) + self.raw = 1 if raw_mode else 0 + if (version is not None) and (re.match('^(\d+).(\d+).(\d+)$', version) is None): + raise ValueError("Version number %r is incorrect - must be in X.Y.Z format" % version) + if isinstance(defaults, dict): + defaults = [ (self.DEFAULT_SECTION_NAME, defaults) ] + self.defaults = defaults + if defaults is not None: + self.reset_to_defaults(save=False) + fname = self.filename() + if backup: + try: + shutil.copyfile(fname, "%s.bak" % fname) + except IOError: + pass + if load: + # If config file already exists, it overrides Default options: + self.load_from_ini() + old_ver = self.get_version(version) + _major = lambda _t: _t[:_t.find('.')] + _minor = lambda _t: _t[:_t.rfind('.')] + # Save new defaults + self._save_new_defaults(defaults, version, subfolder) + # Updating defaults only if major/minor version is different + if _minor(version) != _minor(old_ver): + if backup: + try: + shutil.copyfile(fname, "%s-%s.bak" % (fname, old_ver)) + except IOError: + pass + if check_version(old_ver, '2.4.0', '<'): + self.reset_to_defaults(save=False) + else: + self._update_defaults(defaults, old_ver) + # Remove deprecated options if major version has changed + if remove_obsolete or _major(version) != _major(old_ver): + self._remove_deprecated_options(old_ver) + # Set new version number + self.set_version(version, save=False) + if defaults is None: + # If no defaults are defined, set .ini file settings as default + self.set_as_defaults() + + def get_version(self, version='0.0.0'): + """Return configuration (not application!) version""" + return self.get(self.DEFAULT_SECTION_NAME, 'version', version) + + def set_version(self, version='0.0.0', save=True): + """Set configuration (not application!) version""" + self.set(self.DEFAULT_SECTION_NAME, 'version', version, save=save) + + def load_from_ini(self): + """ + Load config from the associated .ini file + """ + try: + if PY2: + # Python 2 + fname = self.filename() + if osp.isfile(fname): + try: + with codecs.open(fname, encoding='utf-8') as configfile: + self.readfp(configfile) + except IOError: + print("Failed reading file", fname) + else: + # Python 3 + self.read(self.filename(), encoding='utf-8') + except cp.MissingSectionHeaderError: + print("Warning: File contains no section headers.") + + def _load_old_defaults(self, old_version): + """Read old defaults""" + old_defaults = cp.ConfigParser() + if check_version(old_version, '3.0.0', '<='): + path = get_module_source_path('spyder') + else: + path = osp.dirname(self.filename()) + path = osp.join(path, 'defaults') + old_defaults.read(osp.join(path, 'defaults-'+old_version+'.ini')) + return old_defaults + + def _save_new_defaults(self, defaults, new_version, subfolder): + """Save new defaults""" + new_defaults = DefaultsConfig(name='defaults-'+new_version, + subfolder=subfolder) + if not osp.isfile(new_defaults.filename()): + new_defaults.set_defaults(defaults) + new_defaults._save() + + def _update_defaults(self, defaults, old_version, verbose=False): + """Update defaults after a change in version""" + old_defaults = self._load_old_defaults(old_version) + for section, options in defaults: + for option in options: + new_value = options[ option ] + try: + old_value = old_defaults.get(section, option) + except (cp.NoSectionError, cp.NoOptionError): + old_value = None + if old_value is None or \ + to_text_string(new_value) != old_value: + self._set(section, option, new_value, verbose) + + def _remove_deprecated_options(self, old_version): + """ + Remove options which are present in the .ini file but not in defaults + """ + old_defaults = self._load_old_defaults(old_version) + for section in old_defaults.sections(): + for option, _ in old_defaults.items(section, raw=self.raw): + if self.get_default(section, option) is NoDefault: + try: + self.remove_option(section, option) + if len(self.items(section, raw=self.raw)) == 0: + self.remove_section(section) + except cp.NoSectionError: + self.remove_section(section) + + def cleanup(self): + """ + Remove .ini file associated to config + """ + os.remove(self.filename()) + + def set_as_defaults(self): + """ + Set defaults from the current config + """ + self.defaults = [] + for section in self.sections(): + secdict = {} + for option, value in self.items(section, raw=self.raw): + secdict[option] = value + self.defaults.append( (section, secdict) ) + + def reset_to_defaults(self, save=True, verbose=False, section=None): + """ + Reset config to Default values + """ + for sec, options in self.defaults: + if section == None or section == sec: + for option in options: + value = options[ option ] + self._set(sec, option, value, verbose) + if save: + self._save() + + def _check_section_option(self, section, option): + """ + Private method to check section and option types + """ + if section is None: + section = self.DEFAULT_SECTION_NAME + elif not is_text_string(section): + raise RuntimeError("Argument 'section' must be a string") + if not is_text_string(option): + raise RuntimeError("Argument 'option' must be a string") + return section + + def get_default(self, section, option): + """ + Get Default value for a given (section, option) + -> useful for type checking in 'get' method + """ + section = self._check_section_option(section, option) + for sec, options in self.defaults: + if sec == section: + if option in options: + return options[ option ] + else: + return NoDefault + + def get(self, section, option, default=NoDefault): + """ + Get an option + section=None: attribute a default section name + default: default value (if not specified, an exception + will be raised if option doesn't exist) + """ + section = self._check_section_option(section, option) + + if not self.has_section(section): + if default is NoDefault: + raise cp.NoSectionError(section) + else: + self.add_section(section) + + if not self.has_option(section, option): + if default is NoDefault: + raise cp.NoOptionError(option, section) + else: + self.set(section, option, default) + return default + + value = cp.ConfigParser.get(self, section, option, raw=self.raw) + # Use type of default_value to parse value correctly + default_value = self.get_default(section, option) + if isinstance(default_value, bool): + value = ast.literal_eval(value) + elif isinstance(default_value, float): + value = float(value) + elif isinstance(default_value, int): + value = int(value) + else: + if PY2 and is_text_string(default_value): + try: + value = value.decode('utf-8') + except (UnicodeEncodeError, UnicodeDecodeError): + pass + try: + # lists, tuples, ... + value = ast.literal_eval(value) + except (SyntaxError, ValueError): + pass + return value + + def set_default(self, section, option, default_value): + """ + Set Default value for a given (section, option) + -> called when a new (section, option) is set and no default exists + """ + section = self._check_section_option(section, option) + for sec, options in self.defaults: + if sec == section: + options[ option ] = default_value + + def set(self, section, option, value, verbose=False, save=True): + """ + Set an option + section=None: attribute a default section name + """ + section = self._check_section_option(section, option) + default_value = self.get_default(section, option) + if default_value is NoDefault: + # This let us save correctly string value options with + # no config default that contain non-ascii chars in + # Python 2 + if PY2 and is_text_string(value): + value = repr(value) + default_value = value + self.set_default(section, option, default_value) + if isinstance(default_value, bool): + value = bool(value) + elif isinstance(default_value, float): + value = float(value) + elif isinstance(default_value, int): + value = int(value) + elif not is_text_string(default_value): + value = repr(value) + self._set(section, option, value, verbose) + if save: + self._save() + + def remove_section(self, section): + cp.ConfigParser.remove_section(self, section) + self._save() + + def remove_option(self, section, option): + cp.ConfigParser.remove_option(self, section, option) + self._save() diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/config/utils.py spyder-3.0.2+dfsg1/spyder/config/utils.py --- spyder-2.3.8+dfsg1/spyder/config/utils.py 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/config/utils.py 2016-11-16 17:11:22.000000000 +0100 @@ -0,0 +1,171 @@ +# -*- coding: utf-8 -*- +# +# Copyright © Spyder Project Contributors +# Licensed under the terms of the MIT License +# (see spyder/__init__.py for details) + +""" +Utilities to define configuration values +""" + +import os +import os.path as osp +import sys + +from spyder.config.base import _ +from spyder.utils import iofuncs + + +#============================================================================== +# Constants +#============================================================================== +# File types supported by the Editor up to Spyder 2.3 +EDIT_FILETYPES = [ + (_("Python files"), ('.py', '.pyw', '.ipy')), + (_("Cython/Pyrex files"), ('.pyx', '.pxd', '.pxi')), + (_("C files"), ('.c', '.h')), + (_("C++ files"), ('.cc', '.cpp', '.cxx', '.h', '.hh', '.hpp', '.hxx')), + (_("OpenCL files"), ('.cl', )), + (_("Fortran files"), ('.f', '.for', '.f77', '.f90', '.f95', '.f2k')), + (_("IDL files"), ('.pro', )), + (_("MATLAB files"), ('.m', )), + (_("Julia files"), ('.jl',)), + (_("Yaml files"), ('.yaml','.yml',)), + (_("Patch and diff files"), ('.patch', '.diff', '.rej')), + (_("Batch files"), ('.bat', '.cmd')), + (_("Text files"), ('.txt',)), + (_("reStructuredText files"), ('.txt', '.rst')), + (_("gettext files"), ('.po', '.pot')), + (_("NSIS files"), ('.nsi', '.nsh')), + (_("Web page files"), ('.scss', '.css', '.htm', '.html',)), + (_("XML files"), ('.xml',)), + (_("Javascript files"), ('.js',)), + (_("Json files"), ('.json',)), + (_("IPython notebooks"), ('.ipynb',)), + (_("Enaml files"), ('.enaml',)), + (_("Configuration files"), ('.properties', '.session', '.ini', '.inf', + '.reg', '.cfg', '.desktop')), +] + +# Filter for all files +ALL_FILTER = "%s (*)" % _("All files") + +# Extensions supported by Spyder's Variable explorer +IMPORT_EXT = list(iofuncs.iofunctions.load_extensions.values()) + + +#============================================================================== +# Auxiliary functions +#============================================================================== +def _create_filter(title, ftypes): + return "%s (*%s)" % (title, " *".join(ftypes)) + + +def _get_filters(filetypes): + filters = [] + for title, ftypes in filetypes: + filters.append(_create_filter(title, ftypes)) + filters.append(ALL_FILTER) + return ";;".join(filters) + + +def _get_extensions(filetypes): + ftype_list = [] + for _title, ftypes in filetypes: + ftype_list += list(ftypes) + return ftype_list + + +def _get_pygments_extensions(): + """Return all file type extensions supported by Pygments""" + # NOTE: Leave this import here to keep startup process fast! + import pygments.lexers as lexers + + extensions = [] + for lx in lexers.get_all_lexers(): + lexer_exts = lx[2] + + if lexer_exts: + # Reference: This line was included for leaving untrimmed the + # extensions not starting with `*` + other_exts = [le for le in lexer_exts if not le.startswith('*')] + # Reference: This commented line was replaced by the following one + # to trim only extensions that start with '*' + # lexer_exts = [le[1:] for le in lexer_exts] + lexer_exts = [le[1:] for le in lexer_exts if le.startswith('*')] + lexer_exts = [le for le in lexer_exts if not le.endswith('_*')] + extensions = extensions + list(lexer_exts) + list(other_exts) + + return sorted(list(set(extensions))) + + +#============================================================================== +# Main functions +#============================================================================== +def get_filter(filetypes, ext): + """Return filter associated to file extension""" + if not ext: + return ALL_FILTER + for title, ftypes in filetypes: + if ext in ftypes: + return _create_filter(title, ftypes) + else: + return '' + + +def get_edit_filetypes(): + """Get all file types supported by the Editor""" + pygments_exts = _get_pygments_extensions() + other_exts = ['.ipynb', '.md'] + all_exts = tuple(pygments_exts + other_exts) + text_filetypes = (_("Supported text files"), all_exts) + return [text_filetypes] + EDIT_FILETYPES + + +def get_edit_filters(): + """ + Return filters associated with the file types + supported by the Editor + """ + edit_filetypes = get_edit_filetypes() + return _get_filters(edit_filetypes) + + +def get_edit_extensions(): + """ + Return extensions associated with the file types + supported by the Editor + """ + edit_filetypes = get_edit_filetypes() + return _get_extensions(edit_filetypes)+[''] + + +#============================================================================== +# Detection of OS specific versions +#============================================================================== +def is_ubuntu(): + "Detect if we are running in an Ubuntu-based distribution" + if sys.platform.startswith('linux') and osp.isfile('/etc/lsb-release'): + release_info = open('/etc/lsb-release').read() + if 'Ubuntu' in release_info: + return True + else: + return False + else: + return False + + +def is_gtk_desktop(): + "Detect if we are running in a Gtk-based desktop" + if sys.platform.startswith('linux'): + xdg_desktop = os.environ.get('XDG_CURRENT_DESKTOP', '') + if xdg_desktop: + gtk_desktops = ['Unity', 'GNOME', 'XFCE'] + if any([xdg_desktop.startswith(d) for d in gtk_desktops]): + return True + else: + return False + else: + return False + else: + return False diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/config.py spyder-3.0.2+dfsg1/spyder/config.py --- spyder-2.3.8+dfsg1/spyder/config.py 2015-11-27 14:29:54.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/config.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,741 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright © 2009-2011 Pierre Raybaut -# Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) - -""" -Spyder configuration options - -Note: Leave this file free of Qt related imports, so that it can be used to -quickly load a user config file -""" - -import os -import sys -import os.path as osp - -# Local import -from spyderlib.userconfig import UserConfig -from spyderlib.baseconfig import (CHECK_ALL, EXCLUDED_NAMES, SUBFOLDER, - get_home_dir, _) -from spyderlib.utils import iofuncs, codeanalysis - - -#============================================================================== -# Extensions supported by Spyder's Editor -#============================================================================== -EDIT_FILETYPES = ( - (_("Python files"), ('.py', '.pyw', '.ipy')), - (_("Cython/Pyrex files"), ('.pyx', '.pxd', '.pxi')), - (_("C files"), ('.c', '.h')), - (_("C++ files"), ('.cc', '.cpp', '.cxx', '.h', '.hh', '.hpp', '.hxx')), - (_("OpenCL files"), ('.cl', )), - (_("Fortran files"), ('.f', '.for', '.f77', '.f90', '.f95', '.f2k')), - (_("IDL files"), ('.pro', )), - (_("MATLAB files"), ('.m', )), - (_("Julia files"), ('.jl',)), - (_("Yaml files"), ('.yaml','.yml',)), - (_("Patch and diff files"), ('.patch', '.diff', '.rej')), - (_("Batch files"), ('.bat', '.cmd')), - (_("Text files"), ('.txt',)), - (_("reStructured Text files"), ('.txt', '.rst')), - (_("gettext files"), ('.po', '.pot')), - (_("NSIS files"), ('.nsi', '.nsh')), - (_("Web page files"), ('.css', '.htm', '.html',)), - (_("XML files"), ('.xml',)), - (_("Javascript files"), ('.js',)), - (_("Json files"), ('.json',)), - (_("IPython notebooks"), ('.ipynb',)), - (_("Enaml files"), ('.enaml',)), - (_("Configuration files"), ('.properties', '.session', '.ini', '.inf', - '.reg', '.cfg', '.desktop')), - ) - -def _create_filter(title, ftypes): - return "%s (*%s)" % (title, " *".join(ftypes)) - -ALL_FILTER = "%s (*)" % _("All files") - -def _get_filters(filetypes): - filters = [] - for title, ftypes in filetypes: - filters.append(_create_filter(title, ftypes)) - filters.append(ALL_FILTER) - return ";;".join(filters) - -def _get_extensions(filetypes): - ftype_list = [] - for _title, ftypes in filetypes: - ftype_list += list(ftypes) - return ftype_list - -def get_filter(filetypes, ext): - """Return filter associated to file extension""" - if not ext: - return ALL_FILTER - for title, ftypes in filetypes: - if ext in ftypes: - return _create_filter(title, ftypes) - else: - return '' - -EDIT_FILTERS = _get_filters(EDIT_FILETYPES) -EDIT_EXT = _get_extensions(EDIT_FILETYPES)+[''] - -# Extensions supported by Spyder's Variable explorer -IMPORT_EXT = list(iofuncs.iofunctions.load_extensions.values()) - -# Extensions that should be visible in Spyder's file/project explorers -SHOW_EXT = ['.png', '.ico', '.svg'] - -# Extensions supported by Spyder (Editor or Variable explorer) -VALID_EXT = EDIT_EXT+IMPORT_EXT - - -# Find in files include/exclude patterns -INCLUDE_PATTERNS = [r'|'.join(['\\'+_ext+r'$' for _ext in EDIT_EXT if _ext])+\ - r'|README|INSTALL', - r'\.pyw?$|\.ipy$|\.txt$|\.rst$', - '.'] -EXCLUDE_PATTERNS = [r'\.pyc$|\.pyo$|\.orig$|\.hg|\.svn|\bbuild\b', - r'\.pyc$|\.pyo$|\.orig$|\.hg|\.svn'] - - -# Name filters for file/project explorers (excluding files without extension) -NAME_FILTERS = ['*' + _ext for _ext in VALID_EXT + SHOW_EXT if _ext]+\ - ['README', 'INSTALL', 'LICENSE', 'CHANGELOG'] - - -# Port used to detect if there is a running instance and to communicate with -# it to open external files -OPEN_FILES_PORT = 21128 - -# Ctrl key -CTRL = "Meta" if sys.platform == 'darwin' else "Ctrl" - - -#============================================================================== -# Fonts -#============================================================================== -def is_ubuntu(): - "Detect if we are running in an Ubuntu-based distribution" - if sys.platform.startswith('linux') and osp.isfile('/etc/lsb-release'): - release_info = open('/etc/lsb-release').read() - if 'Ubuntu' in release_info: - return True - else: - return False - else: - return False - - -def is_gtk_desktop(): - "Detect if we are running in a Gtk-based desktop" - if sys.platform.startswith('linux'): - xdg_desktop = os.environ.get('XDG_CURRENT_DESKTOP', '') - if xdg_desktop: - gtk_desktops = ['Unity', 'GNOME', 'XFCE'] - if any([xdg_desktop.startswith(d) for d in gtk_desktops]): - return True - else: - return False - else: - return False - else: - return False - - -SANS_SERIF = ['Sans Serif', 'DejaVu Sans', 'Bitstream Vera Sans', - 'Bitstream Charter', 'Lucida Grande', 'MS Shell Dlg 2', - 'Calibri', 'Verdana', 'Geneva', 'Lucid', 'Arial', - 'Helvetica', 'Avant Garde', 'Times', 'sans-serif'] - -MONOSPACE = ['Monospace', 'DejaVu Sans Mono', 'Consolas', - 'Bitstream Vera Sans Mono', 'Andale Mono', 'Liberation Mono', - 'Courier New', 'Courier', 'monospace', 'Fixed', 'Terminal'] - - -if sys.platform == 'darwin': - MONOSPACE = ['Menlo'] + MONOSPACE - BIG = MEDIUM = SMALL = 12 -elif os.name == 'nt': - BIG = 12 - MEDIUM = 10 - SMALL = 9 -elif is_ubuntu(): - SANS_SERIF = ['Ubuntu'] + SANS_SERIF - MONOSPACE = ['Ubuntu Mono'] + MONOSPACE - BIG = 13 - MEDIUM = SMALL = 11 -else: - BIG = 12 - MEDIUM = SMALL = 9 - - -#============================================================================== -# Defaults -#============================================================================== -DEFAULTS = [ - ('main', - { - 'single_instance': True, - 'open_files_port': OPEN_FILES_PORT, - 'tear_off_menus': False, - 'vertical_dockwidget_titlebars': False, - 'vertical_tabs': False, - 'animated_docks': True, - 'window/size': (1260, 740), - 'window/position': (10, 10), - 'window/is_maximized': True, - 'window/is_fullscreen': False, - 'window/prefs_dialog_size': (745, 411), - 'lightwindow/size': (650, 400), - 'lightwindow/position': (30, 30), - 'lightwindow/is_maximized': False, - 'lightwindow/is_fullscreen': False, - - # The following setting is currently not used but necessary from - # a programmatical point of view (see spyder.py): - # (may become useful in the future if we add a button to change - # settings within the "light mode") - 'lightwindow/prefs_dialog_size': (745, 411), - - 'memory_usage/enable': True, - 'memory_usage/timeout': 2000, - 'cpu_usage/enable': False, - 'cpu_usage/timeout': 2000, - 'use_custom_margin': True, - 'custom_margin': 0, - 'show_internal_console_if_traceback': True - }), - ('quick_layouts', - { - 'place_holder': '', - }), - ('editor_appearance', - { - 'cursor/width': 2, - 'completion/size': (300, 180), - }), - ('shell_appearance', - { - 'cursor/width': 2, - 'completion/size': (300, 180), - }), - ('internal_console', - { - 'max_line_count': 300, - 'working_dir_history': 30, - 'working_dir_adjusttocontents': False, - 'font/family': MONOSPACE, - 'font/size': MEDIUM, - 'font/italic': False, - 'font/bold': False, - 'wrap': True, - 'calltips': True, - 'codecompletion/auto': False, - 'codecompletion/enter_key': True, - 'codecompletion/case_sensitive': True, - 'external_editor/path': 'SciTE', - 'external_editor/gotoline': '-goto:', - 'light_background': True, - }), - ('console', - { - 'max_line_count': 500, - 'font/family': MONOSPACE, - 'font/size': MEDIUM, - 'font/italic': False, - 'font/bold': False, - 'wrap': True, - 'single_tab': True, - 'calltips': True, - 'object_inspector': True, - 'codecompletion/auto': True, - 'codecompletion/enter_key': True, - 'codecompletion/case_sensitive': True, - 'show_elapsed_time': False, - 'show_icontext': False, - 'monitor/enabled': True, - 'qt/api': 'default', - 'pyqt/api_version': 2, - 'pyqt/ignore_sip_setapi_errors': False, - 'matplotlib/backend/enabled': True, - 'matplotlib/backend/value': 'MacOSX' if (sys.platform == 'darwin' \ - and os.environ.get('QT_API') == 'pyside')\ - else 'Qt4Agg', - 'umr/enabled': True, - 'umr/verbose': True, - 'umr/namelist': ['guidata', 'guiqwt'], - 'light_background': True, - 'merge_output_channels': os.name != 'nt', - 'colorize_sys_stderr': os.name != 'nt', - 'pythonstartup/default': True, - 'pythonstartup/custom': False, - 'pythonexecutable/default': True, - 'pythonexecutable/custom': False, - 'ets_backend': 'qt4' - }), - ('ipython_console', - { - 'font/family': MONOSPACE, - 'font/size': MEDIUM, - 'font/italic': False, - 'font/bold': False, - 'show_banner': True, - 'use_gui_completion': True, - 'use_pager': False, - 'show_calltips': True, - 'ask_before_closing': True, - 'object_inspector': True, - 'buffer_size': 500, - 'pylab': True, - 'pylab/autoload': False, - 'pylab/backend': 0, - 'pylab/inline/figure_format': 0, - 'pylab/inline/resolution': 72, - 'pylab/inline/width': 6, - 'pylab/inline/height': 4, - 'startup/run_lines': '', - 'startup/use_run_file': False, - 'startup/run_file': '', - 'greedy_completer': False, - 'autocall': 0, - 'symbolic_math': False, - 'in_prompt': '', - 'out_prompt': '', - 'light_color': True, - 'dark_color': False - }), - ('variable_explorer', - { - 'autorefresh': False, - 'autorefresh/timeout': 2000, - 'check_all': CHECK_ALL, - 'excluded_names': EXCLUDED_NAMES, - 'exclude_private': True, - 'exclude_uppercase': True, - 'exclude_capitalized': False, - 'exclude_unsupported': True, - 'truncate': True, - 'minmax': False, - 'remote_editing': False, - }), - ('editor', - { - 'printer_header/font/family': SANS_SERIF, - 'printer_header/font/size': MEDIUM, - 'printer_header/font/italic': False, - 'printer_header/font/bold': False, - 'font/family': MONOSPACE, - 'font/size': MEDIUM, - 'font/italic': False, - 'font/bold': False, - 'wrap': False, - 'wrapflag': True, - 'code_analysis/pyflakes': True, - 'code_analysis/pep8': False, - 'todo_list': True, - 'realtime_analysis': True, - 'realtime_analysis/timeout': 2500, - 'outline_explorer': True, - 'line_numbers': True, - 'blank_spaces': False, - 'edge_line': True, - 'edge_line_column': 79, - 'toolbox_panel': True, - 'calltips': True, - 'go_to_definition': True, - 'close_parentheses': True, - 'close_quotes': False, - 'add_colons': True, - 'auto_unindent': True, - 'indent_chars': '* *', - 'tab_stop_width': 40, - 'object_inspector': True, - 'codecompletion/auto': True, - 'codecompletion/enter_key': True, - 'codecompletion/case_sensitive': True, - 'check_eol_chars': True, - 'tab_always_indent': False, - 'intelligent_backspace': True, - 'highlight_current_line': True, - 'highlight_current_cell': True, - 'occurence_highlighting': True, - 'occurence_highlighting/timeout': 1500, - 'always_remove_trailing_spaces': False, - 'fullpath_sorting': True, - 'show_tab_bar': True, - 'max_recent_files': 20, - 'save_all_before_run': True, - 'focus_to_editor': True, - 'onsave_analysis': False - }), - ('historylog', - { - 'enable': True, - 'max_entries': 100, - 'font/family': MONOSPACE, - 'font/size': MEDIUM, - 'font/italic': False, - 'font/bold': False, - 'wrap': True, - 'go_to_eof': True, - }), - ('inspector', - { - 'enable': True, - 'max_history_entries': 20, - 'font/family': MONOSPACE, - 'font/size': SMALL, - 'font/italic': False, - 'font/bold': False, - 'rich_text/font/family': SANS_SERIF, - 'rich_text/font/size': BIG, - 'rich_text/font/italic': False, - 'rich_text/font/bold': False, - 'wrap': True, - 'connect/editor': False, - 'connect/python_console': False, - 'connect/ipython_console': False, - 'math': True, - 'automatic_import': True, - }), - ('onlinehelp', - { - 'enable': True, - 'zoom_factor': .8, - 'max_history_entries': 20, - }), - ('outline_explorer', - { - 'enable': True, - 'show_fullpath': False, - 'show_all_files': False, - 'show_comments': True, - }), - ('project_explorer', - { - 'enable': True, - 'name_filters': NAME_FILTERS, - 'show_all': False, - 'show_hscrollbar': True - }), - ('arrayeditor', - { - 'font/family': MONOSPACE, - 'font/size': SMALL, - 'font/italic': False, - 'font/bold': False, - }), - ('texteditor', - { - 'font/family': MONOSPACE, - 'font/size': MEDIUM, - 'font/italic': False, - 'font/bold': False, - }), - ('dicteditor', - { - 'font/family': MONOSPACE, - 'font/size': SMALL, - 'font/italic': False, - 'font/bold': False, - }), - ('explorer', - { - 'enable': True, - 'wrap': True, - 'name_filters': NAME_FILTERS, - 'show_hidden': True, - 'show_all': True, - 'show_icontext': False, - }), - ('find_in_files', - { - 'enable': True, - 'supported_encodings': ["utf-8", "iso-8859-1", "cp1252"], - 'include': INCLUDE_PATTERNS, - 'include_regexp': True, - 'exclude': EXCLUDE_PATTERNS, - 'exclude_regexp': True, - 'search_text_regexp': True, - 'search_text': [''], - 'search_text_samples': [codeanalysis.TASKS_PATTERN], - 'in_python_path': False, - 'more_options': True, - }), - ('workingdir', - { - 'editor/open/browse_scriptdir': True, - 'editor/open/browse_workdir': False, - 'editor/new/browse_scriptdir': False, - 'editor/new/browse_workdir': True, - 'editor/open/auto_set_to_basedir': False, - 'editor/save/auto_set_to_basedir': False, - 'working_dir_adjusttocontents': False, - 'working_dir_history': 20, - 'startup/use_last_directory': True, - }), - ('shortcuts', - { - # ---- Global ---- - # -- In spyder.py - '_/close pane': "Shift+Ctrl+F4", - '_/preferences': "Ctrl+Alt+Shift+P", - '_/maximize pane': "Ctrl+Alt+Shift+M", - '_/fullscreen mode': "F11", - '_/quit': "Ctrl+Q", - '_/switch to/from layout 1': "Shift+Alt+F1", - '_/set layout 1': "Ctrl+Shift+Alt+F1", - '_/switch to/from layout 2': "Shift+Alt+F2", - '_/set layout 2': "Ctrl+Shift+Alt+F2", - '_/switch to/from layout 3': "Shift+Alt+F3", - '_/set layout 3': "Ctrl+Shift+Alt+F3", - # -- In plugins/editor - '_/debug step over': "Ctrl+F10", - '_/debug continue': "Ctrl+F12", - '_/debug step into': "Ctrl+F11", - '_/debug step return': "Ctrl+Shift+F11", - '_/debug exit': "Ctrl+Shift+F12", - # -- In plugins/init - '_/switch to inspector': "Ctrl+Shift+H", - '_/switch to outline_explorer': "Ctrl+Shift+O", - '_/switch to editor': "Ctrl+Shift+E", - '_/switch to historylog': "Ctrl+Shift+L", - '_/switch to onlinehelp': "Ctrl+Shift+D", - '_/switch to project_explorer': "Ctrl+Shift+P", - '_/switch to console': "Ctrl+Shift+C", - '_/switch to ipython_console': "Ctrl+Shift+I", - '_/switch to variable_explorer': "Ctrl+Shift+V", - '_/switch to find_in_files': "Ctrl+Shift+F", - '_/switch to explorer': "Ctrl+Shift+X", - # ---- Editor ---- - # -- In codeeditor - 'editor/code completion': CTRL+'+Space', - 'editor/duplicate line': "Ctrl+Alt+Up" if os.name == 'nt' else \ - "Shift+Alt+Up", - 'editor/copy line': "Ctrl+Alt+Down" if os.name == 'nt' else \ - "Shift+Alt+Down", - 'editor/delete line': 'Ctrl+D', - 'editor/move line up': "Alt+Up", - 'editor/move line down': "Alt+Down", - 'editor/go to definition': "Ctrl+G", - 'editor/toggle comment': "Ctrl+1", - 'editor/blockcomment': "Ctrl+4", - 'editor/unblockcomment': "Ctrl+5", - # -- In widgets/editor - 'editor/inspect current object': 'Ctrl+I', - 'editor/go to line': 'Ctrl+L', - 'editor/file list management': 'Ctrl+E', - 'editor/go to previous file': 'Ctrl+Tab', - 'editor/go to next file': 'Ctrl+Shift+Tab', - # -- In spyder.py - 'editor/find text': "Ctrl+F", - 'editor/find next': "F3", - 'editor/find previous': "Shift+F3", - 'editor/replace text': "Ctrl+H", - # -- In plugins/editor - 'editor/show/hide outline': "Ctrl+Alt+O", - 'editor/show/hide project explorer': "Ctrl+Alt+P", - 'editor/new file': "Ctrl+N", - 'editor/open file': "Ctrl+O", - 'editor/save file': "Ctrl+S", - 'editor/save all': "Ctrl+Shift+S", - 'editor/print': "Ctrl+P", - 'editor/close all': "Ctrl+Shift+W", - 'editor/breakpoint': 'F12', - 'editor/conditional breakpoint': 'Shift+F12', - 'editor/debug with winpdb': "F7", - 'editor/debug': "Ctrl+F5", - 'editor/run': "F5", - 'editor/configure': "F6", - 'editor/re-run last script': "Ctrl+F6", - 'editor/run selection': "F9", - 'editor/last edit location': "Ctrl+Alt+Shift+Left", - 'editor/previous cursor position': "Ctrl+Alt+Left", - 'editor/next cursor position': "Ctrl+Alt+Right", - # -- In p_breakpoints - '_/switch to breakpoints': "Ctrl+Shift+B", - # ---- Console (in widgets/shell) ---- - 'console/inspect current object': "Ctrl+I", - 'console/clear shell': "Ctrl+L", - 'console/clear line': "Shift+Escape", - # ---- Pylint (in p_pylint) ---- - 'pylint/run analysis': "F8", - # ---- Profiler (in p_profiler) ---- - 'profiler/run profiler': "F10" - }), - ('color_schemes', - { - 'names': ['Emacs', 'IDLE', 'Monokai', 'Pydev', 'Scintilla', - 'Spyder', 'Spyder/Dark', 'Zenburn'], - # ---- Emacs ---- - # Name Color Bold Italic - 'emacs/background': "#000000", - 'emacs/currentline': "#2b2b43", - 'emacs/currentcell': "#1c1c2d", - 'emacs/occurence': "#abab67", - 'emacs/ctrlclick': "#0000ff", - 'emacs/sideareas': "#555555", - 'emacs/matched_p': "#009800", - 'emacs/unmatched_p': "#c80000", - 'emacs/normal': ('#ffffff', False, False), - 'emacs/keyword': ('#3c51e8', False, False), - 'emacs/builtin': ('#900090', False, False), - 'emacs/definition': ('#ff8040', True, False), - 'emacs/comment': ('#005100', False, False), - 'emacs/string': ('#00aa00', False, True), - 'emacs/number': ('#800000', False, False), - 'emacs/instance': ('#ffffff', False, True), - # ---- IDLE ---- - # Name Color Bold Italic - 'idle/background': "#ffffff", - 'idle/currentline': "#f2e6f3", - 'idle/currentcell': "#feefff", - 'idle/occurence': "#e8f2fe", - 'idle/ctrlclick': "#0000ff", - 'idle/sideareas': "#efefef", - 'idle/matched_p': "#99ff99", - 'idle/unmatched_p': "#ff9999", - 'idle/normal': ('#000000', False, False), - 'idle/keyword': ('#ff7700', True, False), - 'idle/builtin': ('#900090', False, False), - 'idle/definition': ('#0000ff', False, False), - 'idle/comment': ('#dd0000', False, True), - 'idle/string': ('#00aa00', False, False), - 'idle/number': ('#924900', False, False), - 'idle/instance': ('#777777', True, True), - # ---- Monokai ---- - # Name Color Bold Italic - 'monokai/background': "#2a2b24", - 'monokai/currentline': "#484848", - 'monokai/currentcell': "#3d3d3d", - 'monokai/occurence': "#666666", - 'monokai/ctrlclick': "#0000ff", - 'monokai/sideareas': "#2a2b24", - 'monokai/matched_p': "#688060", - 'monokai/unmatched_p': "#bd6e76", - 'monokai/normal': ("#ddddda", False, False), - 'monokai/keyword': ("#f92672", False, False), - 'monokai/builtin': ("#ae81ff", False, False), - 'monokai/definition': ("#a6e22e", False, False), - 'monokai/comment': ("#75715e", False, True), - 'monokai/string': ("#e6db74", False, False), - 'monokai/number': ("#ae81ff", False, False), - 'monokai/instance': ("#ddddda", False, True), - # ---- Pydev ---- - # Name Color Bold Italic - 'pydev/background': "#ffffff", - 'pydev/currentline': "#e8f2fe", - 'pydev/currentcell': "#eff8fe", - 'pydev/occurence': "#ffff99", - 'pydev/ctrlclick': "#0000ff", - 'pydev/sideareas': "#efefef", - 'pydev/matched_p': "#99ff99", - 'pydev/unmatched_p': "#ff99992", - 'pydev/normal': ('#000000', False, False), - 'pydev/keyword': ('#0000ff', False, False), - 'pydev/builtin': ('#900090', False, False), - 'pydev/definition': ('#000000', True, False), - 'pydev/comment': ('#c0c0c0', False, False), - 'pydev/string': ('#00aa00', False, True), - 'pydev/number': ('#800000', False, False), - 'pydev/instance': ('#000000', False, True), - # ---- Scintilla ---- - # Name Color Bold Italic - 'scintilla/background': "#ffffff", - 'scintilla/currentline': "#e1f0d1", - 'scintilla/currentcell': "#edfcdc", - 'scintilla/occurence': "#ffff99", - 'scintilla/ctrlclick': "#0000ff", - 'scintilla/sideareas': "#efefef", - 'scintilla/matched_p': "#99ff99", - 'scintilla/unmatched_p': "#ff9999", - 'scintilla/normal': ('#000000', False, False), - 'scintilla/keyword': ('#00007f', True, False), - 'scintilla/builtin': ('#000000', False, False), - 'scintilla/definition': ('#007f7f', True, False), - 'scintilla/comment': ('#007f00', False, False), - 'scintilla/string': ('#7f007f', False, False), - 'scintilla/number': ('#007f7f', False, False), - 'scintilla/instance': ('#000000', False, True), - # ---- Spyder ---- - # Name Color Bold Italic - 'spyder/background': "#ffffff", - 'spyder/currentline': "#f7ecf8", - 'spyder/currentcell': "#fdfdde", - 'spyder/occurence': "#ffff99", - 'spyder/ctrlclick': "#0000ff", - 'spyder/sideareas': "#efefef", - 'spyder/matched_p': "#99ff99", - 'spyder/unmatched_p': "#ff9999", - 'spyder/normal': ('#000000', False, False), - 'spyder/keyword': ('#0000ff', False, False), - 'spyder/builtin': ('#900090', False, False), - 'spyder/definition': ('#000000', True, False), - 'spyder/comment': ('#adadad', False, True), - 'spyder/string': ('#00aa00', False, False), - 'spyder/number': ('#800000', False, False), - 'spyder/instance': ('#924900', False, True), - # ---- Spyder/Dark ---- - # Name Color Bold Italic - 'spyder/dark/background': "#131926", - 'spyder/dark/currentline': "#2b2b43", - 'spyder/dark/currentcell': "#31314e", - 'spyder/dark/occurence': "#abab67", - 'spyder/dark/ctrlclick': "#0000ff", - 'spyder/dark/sideareas': "#282828", - 'spyder/dark/matched_p': "#009800", - 'spyder/dark/unmatched_p': "#c80000", - 'spyder/dark/normal': ('#ffffff', False, False), - 'spyder/dark/keyword': ('#558eff', False, False), - 'spyder/dark/builtin': ('#aa00aa', False, False), - 'spyder/dark/definition': ('#ffffff', True, False), - 'spyder/dark/comment': ('#7f7f7f', False, False), - 'spyder/dark/string': ('#11a642', False, True), - 'spyder/dark/number': ('#c80000', False, False), - 'spyder/dark/instance': ('#be5f00', False, True), - # ---- Zenburn ---- - # Name Color Bold Italic - 'zenburn/background': "#3f3f3f", - 'zenburn/currentline': "#333333", - 'zenburn/currentcell': "#2c2c2c", - 'zenburn/occurence': "#7a738f", - 'zenburn/ctrlclick': "#0000ff", - 'zenburn/sideareas': "#3f3f3f", - 'zenburn/matched_p': "#688060", - 'zenburn/unmatched_p': "#bd6e76", - 'zenburn/normal': ('#dcdccc', False, False), - 'zenburn/keyword': ('#dfaf8f', True, False), - 'zenburn/builtin': ('#efef8f', False, False), - 'zenburn/definition': ('#efef8f', False, False), - 'zenburn/comment': ('#7f9f7f', False, True), - 'zenburn/string': ('#cc9393', False, False), - 'zenburn/number': ('#8cd0d3', False, False), - 'zenburn/instance': ('#dcdccc', False, True) - }) - ] - - -#============================================================================== -# Config instance -#============================================================================== -# IMPORTANT NOTES: -# 1. If you want to *change* the default value of a current option, you need to -# do a MINOR update in config version, e.g. from 3.0.0 to 3.1.0 -# 2. If you want to *remove* options that are no longer needed in our codebase, -# you need to do a MAJOR update in version, e.g. from 3.0.0 to 4.0.0 -# 3. You don't need to touch this value if you're just adding a new option -CONF_VERSION = '15.2.0' - -# XXX: Previously we had load=(not DEV) here but DEV was set to *False*. -# Check if it *really* needs to be updated or not -CONF = UserConfig('spyder', defaults=DEFAULTS, load=True, version=CONF_VERSION, - subfolder=SUBFOLDER, backup=True, raw_mode=True) - -# Removing old .spyder.ini location: -old_location = osp.join(get_home_dir(), '.spyder.ini') -if osp.isfile(old_location): - os.remove(old_location) diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/defaults/Readme.txt spyder-3.0.2+dfsg1/spyder/defaults/Readme.txt --- spyder-2.3.8+dfsg1/spyder/defaults/Readme.txt 2015-08-24 21:09:44.000000000 +0200 +++ spyder-3.0.2+dfsg1/spyder/defaults/Readme.txt 2016-10-25 02:05:22.000000000 +0200 @@ -1,6 +1,6 @@ -Copyright (c) 2013 The Spyder Development Team +Copyright (c) Spyder Project Contributors Licensed under the terms of the MIT License -(see spyderlib/__init__.py for details) +(see spyder/__init__.py for details) What is the purpose of this directory? ====================================== diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/dependencies.py spyder-3.0.2+dfsg1/spyder/dependencies.py --- spyder-2.3.8+dfsg1/spyder/dependencies.py 2015-11-27 14:29:54.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/dependencies.py 2016-10-25 02:05:22.000000000 +0200 @@ -1,20 +1,20 @@ # -*- coding: utf-8 -*- # -# Copyright © 2013 Pierre Raybaut +# Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) +# (see spyder/__init__.py for details) -"""Module checking Spyder optional runtime dependencies""" +"""Module checking Spyder runtime dependencies""" import os # Local imports -from spyderlib.utils import programs +from spyder.utils import programs class Dependency(object): - """Spyder's optional dependency + """Spyder's dependency version may starts with =, >=, > or < to specify the exact requirement ; multiple conditions may be separated by ';' (e.g. '>=0.13;<1.0')""" @@ -23,15 +23,18 @@ NOK = 'NOK' def __init__(self, modname, features, required_version, - installed_version=None): + installed_version=None, optional=False): self.modname = modname self.features = features self.required_version = required_version + self.optional = optional if installed_version is None: try: self.installed_version = programs.get_module_version(modname) - except ImportError: - # Module is not installed + except: + # NOTE: Don't add any exception type here! + # Modules can fail to import in several ways besides + # ImportError self.installed_version = None else: self.installed_version = installed_version @@ -59,38 +62,52 @@ DEPENDENCIES = [] -def add(modname, features, required_version, installed_version=None): - """Add Spyder optional dependency""" + +def add(modname, features, required_version, installed_version=None, + optional=False): + """Add Spyder dependency""" global DEPENDENCIES for dependency in DEPENDENCIES: if dependency.modname == modname: raise ValueError("Dependency has already been registered: %s"\ % modname) DEPENDENCIES += [Dependency(modname, features, required_version, - installed_version)] + installed_version, optional)] + def check(modname): """Check if required dependency is installed""" - global DEPENDENCIES for dependency in DEPENDENCIES: if dependency.modname == modname: return dependency.check() else: raise RuntimeError("Unkwown dependency %s" % modname) -def status(): - """Return a complete status of Optional Dependencies""" - global DEPENDENCIES + +def status(deps=DEPENDENCIES, linesep=os.linesep): + """Return a status of dependencies""" maxwidth = 0 col1 = [] col2 = [] - for dependency in DEPENDENCIES: + for dependency in deps: title1 = dependency.modname title1 += ' ' + dependency.required_version col1.append(title1) maxwidth = max([maxwidth, len(title1)]) col2.append(dependency.get_installed_version()) text = "" - for index in range(len(DEPENDENCIES)): - text += col1[index].ljust(maxwidth) + ': ' + col2[index] + os.linesep + for index in range(len(deps)): + text += col1[index].ljust(maxwidth) + ': ' + col2[index] + linesep return text + + +def missing_dependencies(): + """Return the status of missing dependencies (if any)""" + missing_deps = [] + for dependency in DEPENDENCIES: + if not dependency.check() and not dependency.optional: + missing_deps.append(dependency) + if missing_deps: + return status(deps=missing_deps, linesep='
') + else: + return "" diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/fonts/spyder-charmap.json spyder-3.0.2+dfsg1/spyder/fonts/spyder-charmap.json --- spyder-2.3.8+dfsg1/spyder/fonts/spyder-charmap.json 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/fonts/spyder-charmap.json 2016-10-25 02:05:22.000000000 +0200 @@ -0,0 +1,49 @@ +{ + "continue": "0xE000", + "debug": "0xE001", + "run-cell": "0xE003", + "run-cell-inplace": "0xE004", + "step-forward": "0xE005", + "step-into": "0xE006", + "step-out": "0xE007", + "step-over": "0xE008", + "stop": "0xE009", + "cell": "0xE00A", + "cell-code": "0xE03B", + "cell-play": "0xE03C", + "cell-next": "0xE03D", + "cell-border": "0xE03E", + "run-one-inplace": "0xE00B", + "run-one": "0xE00C", + "python-logo": "0xE00D", + "python-logo-up": "0xE00E", + "python-logo-down": "0xE00F", + "spyder-logo-web": "0xE010", + "spyder-logo-snake": "0xE011", + "spyder-logo-background": "0xE012", + "inward": "0xE013", + "rows": "0xE014", + "window": "0xE015", + "maximize-pane": "0xE016", + "minimize-pane": "0xE017", + "ipython-logo": "0xE018", + "ipython-logo-alt": "0xE042", + "jupyter-logo": "0xE043", + "run-selection": "0xE019", + "text-select-all": "0xE01A", + "treeview": "0xE01B", + "circle-letter-a": "0xE01F", + "circle-letter-c": "0xE020", + "circle-letter-f": "0xE021", + "circle-hash": "0xE022", + "circle-letter-m": "0xE023", + "circle-percent": "0xE024", + "circle-letter-r": "0xE025", + "circle-underscore": "0xE026", + "cube-front": "0xE055", + "cube-bottom": "0xE056", + "cube-right": "0xE057", + "cube-left": "0xE058", + "cube-rear": "0xE059", + "cube-top": "0xE060" +} Binärdateien spyder-2.3.8+dfsg1/spyder/fonts/spyder.ttf und spyder-3.0.2+dfsg1/spyder/fonts/spyder.ttf sind verschieden. diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/guiconfig.py spyder-3.0.2+dfsg1/spyder/guiconfig.py --- spyder-2.3.8+dfsg1/spyder/guiconfig.py 2015-11-27 14:29:54.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/guiconfig.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,167 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright © 2009-2012 The Spyder development team -# Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) - -""" -Spyder GUI-related configuration management -(for non-GUI configuration, see spyderlib/baseconfig.py) - -Important note regarding shortcuts: - For compatibility with QWERTZ keyboards, one must avoid using the following - shortcuts: - Ctrl + Alt + Q, W, F, G, Y, X, C, V, B, N -""" - -from collections import namedtuple - -from spyderlib.qt.QtGui import QFont, QFontDatabase, QShortcut, QKeySequence -from spyderlib.qt.QtCore import Qt - -from spyderlib.config import CONF -from spyderlib.userconfig import NoDefault -from spyderlib.widgets.sourcecode import syntaxhighlighters as sh -from spyderlib.py3compat import to_text_string - - -# To save metadata about widget shortcuts (needed to build our -# preferences page) -Shortcut = namedtuple('Shortcut', 'data') - - -def font_is_installed(font): - """Check if font is installed""" - return [fam for fam in QFontDatabase().families() - if to_text_string(fam)==font] - - -def get_family(families): - """Return the first installed font family in family list""" - if not isinstance(families, list): - families = [ families ] - for family in families: - if font_is_installed(family): - return family - else: - print("Warning: None of the following fonts is installed: %r" % families) - return QFont().family() - - -FONT_CACHE = {} -def get_font(section, option=None): - """Get console font properties depending on OS and user options""" - font = FONT_CACHE.get((section, option)) - if font is None: - if option is None: - option = 'font' - else: - option += '/font' - families = CONF.get(section, option+"/family", None) - if families is None: - return QFont() - family = get_family(families) - weight = QFont.Normal - italic = CONF.get(section, option+'/italic', False) - if CONF.get(section, option+'/bold', False): - weight = QFont.Bold - size = CONF.get(section, option+'/size', 9) - font = QFont(family, size, weight) - font.setItalic(italic) - FONT_CACHE[(section, option)] = font - return font - - -def set_font(font, section, option=None): - """Set font""" - if option is None: - option = 'font' - else: - option += '/font' - CONF.set(section, option+'/family', to_text_string(font.family())) - CONF.set(section, option+'/size', float(font.pointSize())) - CONF.set(section, option+'/italic', int(font.italic())) - CONF.set(section, option+'/bold', int(font.bold())) - FONT_CACHE[(section, option)] = font - - -def get_shortcut(context, name, default=NoDefault): - """Get keyboard shortcut (key sequence string)""" - return CONF.get('shortcuts', '%s/%s' % (context, name), default=default) - - -def set_shortcut(context, name, keystr): - """Set keyboard shortcut (key sequence string)""" - CONF.set('shortcuts', '%s/%s' % (context, name), keystr) - - -def new_shortcut(keystr, parent, action): - """Define a new shortcut according to a keysequence string""" - sc = QShortcut(QKeySequence(keystr), parent, action) - sc.setContext(Qt.WidgetWithChildrenShortcut) - return sc - - -def create_shortcut(action, context, name, parent): - """Creates a Shortcut namedtuple for a widget""" - keystr = get_shortcut(context, name) - qsc = new_shortcut(keystr, parent, action) - sc = Shortcut(data=(qsc, name, keystr)) - return sc - - -def iter_shortcuts(): - """Iterate over keyboard shortcuts""" - for option in CONF.options('shortcuts'): - context, name = option.split("/", 1) - yield context, name, get_shortcut(context, name) - - -def remove_deprecated_shortcuts(data): - """Remove deprecated shortcuts (shortcuts in CONF but not registered)""" - section = 'shortcuts' - options = [('%s/%s' % (context, name)).lower() for (context, name) in data] - for option, _ in CONF.items(section, raw=CONF.raw): - if option not in options: - CONF.remove_option(section, option) - if len(CONF.items(section, raw=CONF.raw)) == 0: - CONF.remove_section(section) - - -def reset_shortcuts(): - """Reset keyboard shortcuts to default values""" - CONF.reset_to_defaults(section='shortcuts') - - -def get_color_scheme(name): - """Get syntax color scheme""" - color_scheme = {} - for key in sh.COLOR_SCHEME_KEYS: - color_scheme[key] = CONF.get("color_schemes", "%s/%s" % (name, key)) - return color_scheme - - -def set_color_scheme(name, color_scheme, replace=True): - """Set syntax color scheme""" - section = "color_schemes" - names = CONF.get("color_schemes", "names", []) - for key in sh.COLOR_SCHEME_KEYS: - option = "%s/%s" % (name, key) - value = CONF.get(section, option, default=None) - if value is None or replace or name not in names: - CONF.set(section, option, color_scheme[key]) - names.append(to_text_string(name)) - CONF.set(section, "names", sorted(list(set(names)))) - - -def set_default_color_scheme(name, replace=True): - """Reset color scheme to default values""" - assert name in sh.COLOR_SCHEME_NAMES - set_color_scheme(name, sh.get_color_scheme(name), replace=replace) - - -for _name in sh.COLOR_SCHEME_NAMES: - set_default_color_scheme(_name, replace=False) -CUSTOM_COLOR_SCHEME_NAME = "Custom" -set_color_scheme(CUSTOM_COLOR_SCHEME_NAME, sh.get_color_scheme("Spyder"), - replace=False) Binärdateien spyder-2.3.8+dfsg1/spyder/images/actions/1downarrow.png und spyder-3.0.2+dfsg1/spyder/images/actions/1downarrow.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/actions/1uparrow.png und spyder-3.0.2+dfsg1/spyder/images/actions/1uparrow.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/actions/2downarrow.png und spyder-3.0.2+dfsg1/spyder/images/actions/2downarrow.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/actions/2uparrow.png und spyder-3.0.2+dfsg1/spyder/images/actions/2uparrow.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/actions/arrow-continue.png und spyder-3.0.2+dfsg1/spyder/images/actions/arrow-continue.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/actions/arrow-step-in.png und spyder-3.0.2+dfsg1/spyder/images/actions/arrow-step-in.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/actions/arrow-step-out.png und spyder-3.0.2+dfsg1/spyder/images/actions/arrow-step-out.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/actions/arrow-step-over.png und spyder-3.0.2+dfsg1/spyder/images/actions/arrow-step-over.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/actions/auto_reload.png und spyder-3.0.2+dfsg1/spyder/images/actions/auto_reload.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/actions/browse_tab.png und spyder-3.0.2+dfsg1/spyder/images/actions/browse_tab.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/actions/check.png und spyder-3.0.2+dfsg1/spyder/images/actions/check.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/actions/cmdprompt.png und spyder-3.0.2+dfsg1/spyder/images/actions/cmdprompt.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/actions/collapse.png und spyder-3.0.2+dfsg1/spyder/images/actions/collapse.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/actions/collapse_selection.png und spyder-3.0.2+dfsg1/spyder/images/actions/collapse_selection.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/actions/configure.png und spyder-3.0.2+dfsg1/spyder/images/actions/configure.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/actions/copywop.png und spyder-3.0.2+dfsg1/spyder/images/actions/copywop.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/actions/delete.png und spyder-3.0.2+dfsg1/spyder/images/actions/delete.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/actions/edit24.png und spyder-3.0.2+dfsg1/spyder/images/actions/edit24.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/actions/edit_add.png und spyder-3.0.2+dfsg1/spyder/images/actions/edit_add.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/actions/editcopy.png und spyder-3.0.2+dfsg1/spyder/images/actions/editcopy.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/actions/editcut.png und spyder-3.0.2+dfsg1/spyder/images/actions/editcut.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/actions/editdelete.png und spyder-3.0.2+dfsg1/spyder/images/actions/editdelete.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/actions/editpaste.png und spyder-3.0.2+dfsg1/spyder/images/actions/editpaste.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/actions/edit.png und spyder-3.0.2+dfsg1/spyder/images/actions/edit.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/actions/edit_remove.png und spyder-3.0.2+dfsg1/spyder/images/actions/edit_remove.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/actions/eraser.png und spyder-3.0.2+dfsg1/spyder/images/actions/eraser.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/actions/exit.png und spyder-3.0.2+dfsg1/spyder/images/actions/exit.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/actions/expand.png und spyder-3.0.2+dfsg1/spyder/images/actions/expand.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/actions/expand_selection.png und spyder-3.0.2+dfsg1/spyder/images/actions/expand_selection.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/actions/filter.png und spyder-3.0.2+dfsg1/spyder/images/actions/filter.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/actions/findf.png und spyder-3.0.2+dfsg1/spyder/images/actions/findf.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/actions/findnext.png und spyder-3.0.2+dfsg1/spyder/images/actions/findnext.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/actions/find.png und spyder-3.0.2+dfsg1/spyder/images/actions/find.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/actions/findprevious.png und spyder-3.0.2+dfsg1/spyder/images/actions/findprevious.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/actions/folder_new.png und spyder-3.0.2+dfsg1/spyder/images/actions/folder_new.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/actions/hide.png und spyder-3.0.2+dfsg1/spyder/images/actions/hide.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/actions/hist.png und spyder-3.0.2+dfsg1/spyder/images/actions/hist.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/actions/home.png und spyder-3.0.2+dfsg1/spyder/images/actions/home.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/actions/imshow.png und spyder-3.0.2+dfsg1/spyder/images/actions/imshow.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/actions/insert.png und spyder-3.0.2+dfsg1/spyder/images/actions/insert.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/actions/lock_open.png und spyder-3.0.2+dfsg1/spyder/images/actions/lock_open.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/actions/lock.png und spyder-3.0.2+dfsg1/spyder/images/actions/lock.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/actions/magnifier.png und spyder-3.0.2+dfsg1/spyder/images/actions/magnifier.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/actions/maximize.png und spyder-3.0.2+dfsg1/spyder/images/actions/maximize.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/actions/next.png und spyder-3.0.2+dfsg1/spyder/images/actions/next.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/actions/options_less.png und spyder-3.0.2+dfsg1/spyder/images/actions/options_less.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/actions/options_more.png und spyder-3.0.2+dfsg1/spyder/images/actions/options_more.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/actions/plot.png und spyder-3.0.2+dfsg1/spyder/images/actions/plot.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/actions/previous.png und spyder-3.0.2+dfsg1/spyder/images/actions/previous.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/actions/redo.png und spyder-3.0.2+dfsg1/spyder/images/actions/redo.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/actions/reload.png und spyder-3.0.2+dfsg1/spyder/images/actions/reload.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/actions/rename.png und spyder-3.0.2+dfsg1/spyder/images/actions/rename.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/actions/replace.png und spyder-3.0.2+dfsg1/spyder/images/actions/replace.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/actions/restore.png und spyder-3.0.2+dfsg1/spyder/images/actions/restore.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/actions/show.png und spyder-3.0.2+dfsg1/spyder/images/actions/show.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/actions/special_paste.png und spyder-3.0.2+dfsg1/spyder/images/actions/special_paste.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/actions/stop_debug.png und spyder-3.0.2+dfsg1/spyder/images/actions/stop_debug.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/actions/stop.png und spyder-3.0.2+dfsg1/spyder/images/actions/stop.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/actions/synchronize.png und spyder-3.0.2+dfsg1/spyder/images/actions/synchronize.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/actions/tooloptions.png und spyder-3.0.2+dfsg1/spyder/images/actions/tooloptions.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/actions/undo.png und spyder-3.0.2+dfsg1/spyder/images/actions/undo.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/actions/unmaximize.png und spyder-3.0.2+dfsg1/spyder/images/actions/unmaximize.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/actions/up.png und spyder-3.0.2+dfsg1/spyder/images/actions/up.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/actions/window_fullscreen.png und spyder-3.0.2+dfsg1/spyder/images/actions/window_fullscreen.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/actions/window_nofullscreen.png und spyder-3.0.2+dfsg1/spyder/images/actions/window_nofullscreen.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/actions/zoom_in.png und spyder-3.0.2+dfsg1/spyder/images/actions/zoom_in.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/actions/zoom_out.png und spyder-3.0.2+dfsg1/spyder/images/actions/zoom_out.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/advanced.png und spyder-3.0.2+dfsg1/spyder/images/advanced.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/arredit.png und spyder-3.0.2+dfsg1/spyder/images/arredit.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/arrow.png und spyder-3.0.2+dfsg1/spyder/images/arrow.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/bold.png und spyder-3.0.2+dfsg1/spyder/images/bold.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/browser.png und spyder-3.0.2+dfsg1/spyder/images/browser.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/console/clear.png und spyder-3.0.2+dfsg1/spyder/images/console/clear.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/console/cmdprompt_t.png und spyder-3.0.2+dfsg1/spyder/images/console/cmdprompt_t.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/console/console.png und spyder-3.0.2+dfsg1/spyder/images/console/console.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/console/editclear.png und spyder-3.0.2+dfsg1/spyder/images/console/editclear.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/console/environ.png und spyder-3.0.2+dfsg1/spyder/images/console/environ.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/console/history24.png und spyder-3.0.2+dfsg1/spyder/images/console/history24.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/console/history.png und spyder-3.0.2+dfsg1/spyder/images/console/history.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/console/ipython_console.png und spyder-3.0.2+dfsg1/spyder/images/console/ipython_console.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/console/ipython_console_t.png und spyder-3.0.2+dfsg1/spyder/images/console/ipython_console_t.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/console/kill.png und spyder-3.0.2+dfsg1/spyder/images/console/kill.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/console/loading_sprites.png und spyder-3.0.2+dfsg1/spyder/images/console/loading_sprites.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/console/prompt.png und spyder-3.0.2+dfsg1/spyder/images/console/prompt.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/console/python.png und spyder-3.0.2+dfsg1/spyder/images/console/python.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/console/python_t.png und spyder-3.0.2+dfsg1/spyder/images/console/python_t.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/console/restart.png und spyder-3.0.2+dfsg1/spyder/images/console/restart.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/console/run_small.png und spyder-3.0.2+dfsg1/spyder/images/console/run_small.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/console/syspath.png und spyder-3.0.2+dfsg1/spyder/images/console/syspath.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/console/terminated.png und spyder-3.0.2+dfsg1/spyder/images/console/terminated.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/dictedit.png und spyder-3.0.2+dfsg1/spyder/images/dictedit.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/editor/attribute.png und spyder-3.0.2+dfsg1/spyder/images/editor/attribute.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/editor/blockcomment.png und spyder-3.0.2+dfsg1/spyder/images/editor/blockcomment.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/editor/breakpoint_big.png und spyder-3.0.2+dfsg1/spyder/images/editor/breakpoint_big.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/editor/breakpoint_cond_big.png und spyder-3.0.2+dfsg1/spyder/images/editor/breakpoint_cond_big.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/editor/breakpoint_cond_small.png und spyder-3.0.2+dfsg1/spyder/images/editor/breakpoint_cond_small.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/editor/breakpoint_small.png und spyder-3.0.2+dfsg1/spyder/images/editor/breakpoint_small.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/editor/bug.png und spyder-3.0.2+dfsg1/spyder/images/editor/bug.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/editor/cell.png und spyder-3.0.2+dfsg1/spyder/images/editor/cell.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/editor/class.png und spyder-3.0.2+dfsg1/spyder/images/editor/class.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/editor/close_panel.png und spyder-3.0.2+dfsg1/spyder/images/editor/close_panel.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/editor/comment.png und spyder-3.0.2+dfsg1/spyder/images/editor/comment.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/editor/convention.png und spyder-3.0.2+dfsg1/spyder/images/editor/convention.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/editor/debug.png und spyder-3.0.2+dfsg1/spyder/images/editor/debug.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/editor/error.png und spyder-3.0.2+dfsg1/spyder/images/editor/error.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/editor/filelist.png und spyder-3.0.2+dfsg1/spyder/images/editor/filelist.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/editor/file.png und spyder-3.0.2+dfsg1/spyder/images/editor/file.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/editor/fromcursor.png und spyder-3.0.2+dfsg1/spyder/images/editor/fromcursor.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/editor/function.png und spyder-3.0.2+dfsg1/spyder/images/editor/function.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/editor/gotoline.png und spyder-3.0.2+dfsg1/spyder/images/editor/gotoline.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/editor/highlight.png und spyder-3.0.2+dfsg1/spyder/images/editor/highlight.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/editor/horsplit.png und spyder-3.0.2+dfsg1/spyder/images/editor/horsplit.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/editor/indent.png und spyder-3.0.2+dfsg1/spyder/images/editor/indent.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/editor/last_edit_location.png und spyder-3.0.2+dfsg1/spyder/images/editor/last_edit_location.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/editor/method.png und spyder-3.0.2+dfsg1/spyder/images/editor/method.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/editor/module.png und spyder-3.0.2+dfsg1/spyder/images/editor/module.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/editor/newwindow.png und spyder-3.0.2+dfsg1/spyder/images/editor/newwindow.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/editor/next_cursor.png und spyder-3.0.2+dfsg1/spyder/images/editor/next_cursor.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/editor/next_wng.png und spyder-3.0.2+dfsg1/spyder/images/editor/next_wng.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/editor/no_match.png und spyder-3.0.2+dfsg1/spyder/images/editor/no_match.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/editor/outline_explorer.png und spyder-3.0.2+dfsg1/spyder/images/editor/outline_explorer.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/editor/outline_explorer_vis.png und spyder-3.0.2+dfsg1/spyder/images/editor/outline_explorer_vis.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/editor/prev_cursor.png und spyder-3.0.2+dfsg1/spyder/images/editor/prev_cursor.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/editor/prev_wng.png und spyder-3.0.2+dfsg1/spyder/images/editor/prev_wng.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/editor/private1.png und spyder-3.0.2+dfsg1/spyder/images/editor/private1.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/editor/private2.png und spyder-3.0.2+dfsg1/spyder/images/editor/private2.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/editor/refactor.png und spyder-3.0.2+dfsg1/spyder/images/editor/refactor.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/editor/run_again.png und spyder-3.0.2+dfsg1/spyder/images/editor/run_again.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/editor/run_cell_advance.png und spyder-3.0.2+dfsg1/spyder/images/editor/run_cell_advance.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/editor/run_cell.png und spyder-3.0.2+dfsg1/spyder/images/editor/run_cell.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/editor/run.png und spyder-3.0.2+dfsg1/spyder/images/editor/run.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/editor/run_selection.png und spyder-3.0.2+dfsg1/spyder/images/editor/run_selection.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/editor/run_settings.png und spyder-3.0.2+dfsg1/spyder/images/editor/run_settings.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/editor/selectall.png und spyder-3.0.2+dfsg1/spyder/images/editor/selectall.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/editor/select.png und spyder-3.0.2+dfsg1/spyder/images/editor/select.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/editor/todo_list.png und spyder-3.0.2+dfsg1/spyder/images/editor/todo_list.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/editor/todo.png und spyder-3.0.2+dfsg1/spyder/images/editor/todo.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/editor/uncomment.png und spyder-3.0.2+dfsg1/spyder/images/editor/uncomment.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/editor/unindent.png und spyder-3.0.2+dfsg1/spyder/images/editor/unindent.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/editor/versplit.png und spyder-3.0.2+dfsg1/spyder/images/editor/versplit.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/editor/warning.png und spyder-3.0.2+dfsg1/spyder/images/editor/warning.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/editor/wng_list.png und spyder-3.0.2+dfsg1/spyder/images/editor/wng_list.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/eyedropper.png und spyder-3.0.2+dfsg1/spyder/images/eyedropper.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/file/filecloseall.png und spyder-3.0.2+dfsg1/spyder/images/file/filecloseall.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/file/fileclose.png und spyder-3.0.2+dfsg1/spyder/images/file/fileclose.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/file/fileimport.png und spyder-3.0.2+dfsg1/spyder/images/file/fileimport.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/file/filenew.png und spyder-3.0.2+dfsg1/spyder/images/file/filenew.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/file/fileopen.png und spyder-3.0.2+dfsg1/spyder/images/file/fileopen.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/file/filesaveas.png und spyder-3.0.2+dfsg1/spyder/images/file/filesaveas.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/file/filesave.png und spyder-3.0.2+dfsg1/spyder/images/file/filesave.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/file/print.png und spyder-3.0.2+dfsg1/spyder/images/file/print.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/file/save_all.png und spyder-3.0.2+dfsg1/spyder/images/file/save_all.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/filetypes/bat.png und spyder-3.0.2+dfsg1/spyder/images/filetypes/bat.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/filetypes/bmp.png und spyder-3.0.2+dfsg1/spyder/images/filetypes/bmp.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/filetypes/cc.png und spyder-3.0.2+dfsg1/spyder/images/filetypes/cc.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/filetypes/cfg.png und spyder-3.0.2+dfsg1/spyder/images/filetypes/cfg.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/filetypes/chm.png und spyder-3.0.2+dfsg1/spyder/images/filetypes/chm.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/filetypes/cl.png und spyder-3.0.2+dfsg1/spyder/images/filetypes/cl.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/filetypes/cmd.png und spyder-3.0.2+dfsg1/spyder/images/filetypes/cmd.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/filetypes/c.png und spyder-3.0.2+dfsg1/spyder/images/filetypes/c.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/filetypes/cpp.png und spyder-3.0.2+dfsg1/spyder/images/filetypes/cpp.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/filetypes/css.png und spyder-3.0.2+dfsg1/spyder/images/filetypes/css.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/filetypes/cxx.png und spyder-3.0.2+dfsg1/spyder/images/filetypes/cxx.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/filetypes/diff.png und spyder-3.0.2+dfsg1/spyder/images/filetypes/diff.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/filetypes/doc.png und spyder-3.0.2+dfsg1/spyder/images/filetypes/doc.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/filetypes/enaml.png und spyder-3.0.2+dfsg1/spyder/images/filetypes/enaml.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/filetypes/exe.png und spyder-3.0.2+dfsg1/spyder/images/filetypes/exe.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/filetypes/f77.png und spyder-3.0.2+dfsg1/spyder/images/filetypes/f77.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/filetypes/f90.png und spyder-3.0.2+dfsg1/spyder/images/filetypes/f90.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/filetypes/f.png und spyder-3.0.2+dfsg1/spyder/images/filetypes/f.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/filetypes/gif.png und spyder-3.0.2+dfsg1/spyder/images/filetypes/gif.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/filetypes/hh.png und spyder-3.0.2+dfsg1/spyder/images/filetypes/hh.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/filetypes/h.png und spyder-3.0.2+dfsg1/spyder/images/filetypes/h.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/filetypes/hpp.png und spyder-3.0.2+dfsg1/spyder/images/filetypes/hpp.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/filetypes/html.png und spyder-3.0.2+dfsg1/spyder/images/filetypes/html.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/filetypes/htm.png und spyder-3.0.2+dfsg1/spyder/images/filetypes/htm.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/filetypes/hxx.png und spyder-3.0.2+dfsg1/spyder/images/filetypes/hxx.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/filetypes/inf.png und spyder-3.0.2+dfsg1/spyder/images/filetypes/inf.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/filetypes/ini.png und spyder-3.0.2+dfsg1/spyder/images/filetypes/ini.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/filetypes/jl.png und spyder-3.0.2+dfsg1/spyder/images/filetypes/jl.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/filetypes/jpeg.png und spyder-3.0.2+dfsg1/spyder/images/filetypes/jpeg.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/filetypes/jpg.png und spyder-3.0.2+dfsg1/spyder/images/filetypes/jpg.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/filetypes/js.png und spyder-3.0.2+dfsg1/spyder/images/filetypes/js.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/filetypes/log.png und spyder-3.0.2+dfsg1/spyder/images/filetypes/log.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/filetypes/nsh.png und spyder-3.0.2+dfsg1/spyder/images/filetypes/nsh.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/filetypes/nsi.png und spyder-3.0.2+dfsg1/spyder/images/filetypes/nsi.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/filetypes/nt.png und spyder-3.0.2+dfsg1/spyder/images/filetypes/nt.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/filetypes/patch.png und spyder-3.0.2+dfsg1/spyder/images/filetypes/patch.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/filetypes/pdf.png und spyder-3.0.2+dfsg1/spyder/images/filetypes/pdf.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/filetypes/png.png und spyder-3.0.2+dfsg1/spyder/images/filetypes/png.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/filetypes/po.png und spyder-3.0.2+dfsg1/spyder/images/filetypes/po.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/filetypes/pot.png und spyder-3.0.2+dfsg1/spyder/images/filetypes/pot.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/filetypes/pps.png und spyder-3.0.2+dfsg1/spyder/images/filetypes/pps.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/filetypes/properties.png und spyder-3.0.2+dfsg1/spyder/images/filetypes/properties.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/filetypes/ps.png und spyder-3.0.2+dfsg1/spyder/images/filetypes/ps.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/filetypes/pxd.png und spyder-3.0.2+dfsg1/spyder/images/filetypes/pxd.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/filetypes/pxi.png und spyder-3.0.2+dfsg1/spyder/images/filetypes/pxi.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/filetypes/pyc.png und spyder-3.0.2+dfsg1/spyder/images/filetypes/pyc.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/filetypes/py.png und spyder-3.0.2+dfsg1/spyder/images/filetypes/py.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/filetypes/pyw.png und spyder-3.0.2+dfsg1/spyder/images/filetypes/pyw.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/filetypes/pyx.png und spyder-3.0.2+dfsg1/spyder/images/filetypes/pyx.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/filetypes/rar.png und spyder-3.0.2+dfsg1/spyder/images/filetypes/rar.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/filetypes/readme.png und spyder-3.0.2+dfsg1/spyder/images/filetypes/readme.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/filetypes/reg.png und spyder-3.0.2+dfsg1/spyder/images/filetypes/reg.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/filetypes/rej.png und spyder-3.0.2+dfsg1/spyder/images/filetypes/rej.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/filetypes/scss.png und spyder-3.0.2+dfsg1/spyder/images/filetypes/scss.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/filetypes/session.png und spyder-3.0.2+dfsg1/spyder/images/filetypes/session.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/filetypes/tar.png und spyder-3.0.2+dfsg1/spyder/images/filetypes/tar.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/filetypes/tex.png und spyder-3.0.2+dfsg1/spyder/images/filetypes/tex.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/filetypes/tgz.png und spyder-3.0.2+dfsg1/spyder/images/filetypes/tgz.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/filetypes/tiff.png und spyder-3.0.2+dfsg1/spyder/images/filetypes/tiff.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/filetypes/tif.png und spyder-3.0.2+dfsg1/spyder/images/filetypes/tif.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/filetypes/ts.png und spyder-3.0.2+dfsg1/spyder/images/filetypes/ts.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/filetypes/txt.png und spyder-3.0.2+dfsg1/spyder/images/filetypes/txt.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/filetypes/ui.png und spyder-3.0.2+dfsg1/spyder/images/filetypes/ui.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/filetypes/xls.png und spyder-3.0.2+dfsg1/spyder/images/filetypes/xls.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/filetypes/xml.png und spyder-3.0.2+dfsg1/spyder/images/filetypes/xml.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/filetypes/zip.png und spyder-3.0.2+dfsg1/spyder/images/filetypes/zip.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/font.png und spyder-3.0.2+dfsg1/spyder/images/font.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/genprefs.png und spyder-3.0.2+dfsg1/spyder/images/genprefs.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/help.png und spyder-3.0.2+dfsg1/spyder/images/help.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/inspector.png und spyder-3.0.2+dfsg1/spyder/images/inspector.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/italic.png und spyder-3.0.2+dfsg1/spyder/images/italic.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/keyboard.png und spyder-3.0.2+dfsg1/spyder/images/keyboard.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/matplotlib.png und spyder-3.0.2+dfsg1/spyder/images/matplotlib.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/none.png und spyder-3.0.2+dfsg1/spyder/images/none.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/not_found.png und spyder-3.0.2+dfsg1/spyder/images/not_found.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/projects/add_to_path.png und spyder-3.0.2+dfsg1/spyder/images/projects/add_to_path.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/projects/folder.png und spyder-3.0.2+dfsg1/spyder/images/projects/folder.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/projects/package.png und spyder-3.0.2+dfsg1/spyder/images/projects/package.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/projects/pp_folder.png und spyder-3.0.2+dfsg1/spyder/images/projects/pp_folder.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/projects/pp_package.png und spyder-3.0.2+dfsg1/spyder/images/projects/pp_package.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/projects/pp_project.png und spyder-3.0.2+dfsg1/spyder/images/projects/pp_project.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/projects/project_closed.png und spyder-3.0.2+dfsg1/spyder/images/projects/project_closed.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/projects/project.png und spyder-3.0.2+dfsg1/spyder/images/projects/project.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/projects/pydev.png und spyder-3.0.2+dfsg1/spyder/images/projects/pydev.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/projects/pythonpath.png und spyder-3.0.2+dfsg1/spyder/images/projects/pythonpath.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/projects/remove_from_path.png und spyder-3.0.2+dfsg1/spyder/images/projects/remove_from_path.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/projects/show_all.png und spyder-3.0.2+dfsg1/spyder/images/projects/show_all.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/pythonpath_mgr.png und spyder-3.0.2+dfsg1/spyder/images/pythonpath_mgr.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/pythonpath.png und spyder-3.0.2+dfsg1/spyder/images/pythonpath.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/pythonxy.png und spyder-3.0.2+dfsg1/spyder/images/pythonxy.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/qtassistant.png und spyder-3.0.2+dfsg1/spyder/images/qtassistant.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/qtdesigner.png und spyder-3.0.2+dfsg1/spyder/images/qtdesigner.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/qtlinguist.png und spyder-3.0.2+dfsg1/spyder/images/qtlinguist.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/qt.png und spyder-3.0.2+dfsg1/spyder/images/qt.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/scipy.png und spyder-3.0.2+dfsg1/spyder/images/scipy.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/set_workdir.png und spyder-3.0.2+dfsg1/spyder/images/set_workdir.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/splash.png und spyder-3.0.2+dfsg1/spyder/images/splash.png sind verschieden. diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/images/splash.svg spyder-3.0.2+dfsg1/spyder/images/splash.svg --- spyder-2.3.8+dfsg1/spyder/images/splash.svg 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/images/splash.svg 2016-10-25 02:05:23.000000000 +0200 @@ -0,0 +1,3788 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spyder + 3 + + diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/images/spyder_light.svg spyder-3.0.2+dfsg1/spyder/images/spyder_light.svg --- spyder-2.3.8+dfsg1/spyder/images/spyder_light.svg 2015-08-24 21:09:44.000000000 +0200 +++ spyder-3.0.2+dfsg1/spyder/images/spyder_light.svg 1970-01-01 01:00:00.000000000 +0100 @@ -1,366 +0,0 @@ - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Binärdateien spyder-2.3.8+dfsg1/spyder/images/spyder.png und spyder-3.0.2+dfsg1/spyder/images/spyder.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/tour-spyder-logo.png und spyder-3.0.2+dfsg1/spyder/images/tour-spyder-logo.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/upper_lower.png und spyder-3.0.2+dfsg1/spyder/images/upper_lower.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/vcs_browse.png und spyder-3.0.2+dfsg1/spyder/images/vcs_browse.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/vcs_commit.png und spyder-3.0.2+dfsg1/spyder/images/vcs_commit.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/vitables.png und spyder-3.0.2+dfsg1/spyder/images/vitables.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/whole_words.png und spyder-3.0.2+dfsg1/spyder/images/whole_words.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/images/win_env.png und spyder-3.0.2+dfsg1/spyder/images/win_env.png sind verschieden. diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/__init__.py spyder-3.0.2+dfsg1/spyder/__init__.py --- spyder-2.3.8+dfsg1/spyder/__init__.py 2016-12-15 15:19:55.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/__init__.py 2016-12-15 15:20:01.000000000 +0100 @@ -3,8 +3,7 @@ Spyder License Agreement (MIT License) -------------------------------------- -Copyright (c) 2009-2013 Pierre Raybaut -Copyright (c) 2013-2015 The Spyder Development Team +Copyright (c) Spyder Project Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation @@ -28,7 +27,9 @@ OTHER DEALINGS IN THE SOFTWARE. """ -__version__ = '2.3.8' +version_info = (3, 0, 2) + +__version__ = '.'.join(map(str, version_info)) __license__ = __doc__ __project_url__ = 'https://github.com/spyder-ide/spyder' __forum_url__ = 'http://groups.google.com/group/spyderlib' @@ -36,8 +37,9 @@ # Dear (Debian, RPM, ...) package makers, please feel free to customize the # following path to module's data (images) and translations: DATAPATH = LOCALEPATH = DOCPATH = MATHJAXPATH = JQUERYPATH = '' -DATAPATH = '/usr/share/spyderlib/images' -DOCPATH = '/usr/share/doc/python-spyderlib-doc/html' +DATAPATH = '/usr/share/spyder/images' +LOCALEPATH = '/usr/share/spyder/locale' +DOCPATH = '/usr/share/doc/spyder-doc/html' MATHJAXPATH = '/usr/share/javascript/mathjax' JQUERYPATH = '/usr/share/javascript/jquery' @@ -54,7 +56,7 @@ dist.add_qt_bindings() except AttributeError: raise ImportError("This script requires guidata 1.5+") - for _modname in ('spyderlib', 'spyderplugins'): + for _modname in ('spyder', 'spyderplugins'): dist.add_module_data_files(_modname, ("", ), ('.png', '.svg', '.html', '.png', '.txt', '.js', '.inv', '.ico', '.css', '.doctree', @@ -66,26 +68,27 @@ """Get version information for components used by Spyder""" import sys import platform - import spyderlib.qt - import spyderlib.qt.QtCore + + import qtpy + import qtpy.QtCore revision = None if reporev: - from spyderlib.utils import vcs - revision = vcs.get_git_revision(os.path.dirname(__dir__)) + from spyder.utils import vcs + revision, branch = vcs.get_git_revision(os.path.dirname(__dir__)) if not sys.platform == 'darwin': # To avoid a crash with our Mac app system = platform.system() else: system = 'Darwin' - + return { 'spyder': __version__, 'python': platform.python_version(), # "2.7.3" 'bitness': 64 if sys.maxsize > 2**32 else 32, - 'qt': spyderlib.qt.QtCore.__version__, - 'qt_api': spyderlib.qt.API_NAME, # PySide or PyQt4 - 'qt_api_ver': spyderlib.qt.__version__, + 'qt': qtpy.QtCore.__version__, + 'qt_api': qtpy.API_NAME, # PyQt5 or PyQt4 + 'qt_api_ver': qtpy.PYQT_VERSION, 'system': system, # Linux, Windows, ... 'revision': revision, # '9fdf926eccce' } diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/interpreter.py spyder-3.0.2+dfsg1/spyder/interpreter.py --- spyder-2.3.8+dfsg1/spyder/interpreter.py 2015-10-12 00:53:22.000000000 +0200 +++ spyder-3.0.2+dfsg1/spyder/interpreter.py 2016-10-25 02:05:23.000000000 +0200 @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- # -# Copyright © 2009-2010 Pierre Raybaut +# Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) +# (see spyder/__init__.py for details) """Shell Interpreter""" @@ -16,14 +16,13 @@ import re import os.path as osp import pydoc -from subprocess import Popen, PIPE from code import InteractiveConsole # Local imports: -from spyderlib.utils.dochelpers import isdefined -from spyderlib.utils import encoding -from spyderlib.py3compat import is_text_string, getcwd -from spyderlib.utils.misc import remove_backslashes +from spyder.utils.dochelpers import isdefined +from spyder.utils import encoding, programs +from spyder.py3compat import is_text_string, getcwd +from spyder.utils.misc import remove_backslashes # Force Python to search modules in the current directory first: sys.path.insert(0, '') @@ -211,8 +210,7 @@ # Execute command elif cmd.startswith('!'): # System ! command - pipe = Popen(cmd[1:], shell=True, - stdin=PIPE, stderr=PIPE, stdout=PIPE) + pipe = programs.run_shell_command(cmd[1:]) txt_out = encoding.transcode( pipe.stdout.read().decode() ) txt_err = encoding.transcode( pipe.stderr.read().decode().rstrip() ) if txt_err: @@ -334,4 +332,4 @@ def resetbuffer(self): """Remove any unhandled source text from the input buffer""" InteractiveConsole.resetbuffer(self) - \ Kein Zeilenumbruch am Dateiende. + diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/ipythonconfig.py spyder-3.0.2+dfsg1/spyder/ipythonconfig.py --- spyder-2.3.8+dfsg1/spyder/ipythonconfig.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/ipythonconfig.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,55 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright © 2013 The Spyder Development Team -# Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) - -""" -IPython configuration variables needed by Spyder -""" - -from spyderlib.utils import programs -from spyderlib import dependencies -from spyderlib.baseconfig import _ - - -# Constants -IPYTHON_REQVER = '>=1.0' -ZMQ_REQVER = '>=2.1.11' -QTCONSOLE_REQVER = '>=4.0' - - -# Dependencies -dependencies.add("IPython", _("IPython Console integration"), - required_version=IPYTHON_REQVER) -dependencies.add("zmq", _("IPython Console integration"), - required_version=ZMQ_REQVER) - - -# Jupyter 4.0 requirements -ipy4_installed = programs.is_module_installed('IPython', '>=4.0') -if ipy4_installed: - dependencies.add("qtconsole", _("IPython Console integration"), - required_version=QTCONSOLE_REQVER) - - -# Auxiliary functions -def is_qtconsole_installed(): - pyzmq_installed = programs.is_module_installed('zmq') - pygments_installed = programs.is_module_installed('pygments') - ipyqt_installed = programs.is_module_installed('IPython.qt') - - if ipyqt_installed and pyzmq_installed and pygments_installed: - if ipy4_installed: - if programs.is_module_installed('qtconsole'): - return True - else: - return False - else: - return True - else: - return False - - -# Main check for IPython presence -IPYTHON_QT_INSTALLED = is_qtconsole_installed() Binärdateien spyder-2.3.8+dfsg1/spyder/locale/es/LC_MESSAGES/spyderlib.mo und spyder-3.0.2+dfsg1/spyder/locale/es/LC_MESSAGES/spyderlib.mo sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/locale/es/LC_MESSAGES/spyder.mo und spyder-3.0.2+dfsg1/spyder/locale/es/LC_MESSAGES/spyder.mo sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/locale/fr/LC_MESSAGES/spyderlib.mo und spyder-3.0.2+dfsg1/spyder/locale/fr/LC_MESSAGES/spyderlib.mo sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/locale/fr/LC_MESSAGES/spyder.mo und spyder-3.0.2+dfsg1/spyder/locale/fr/LC_MESSAGES/spyder.mo sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/locale/ja/LC_MESSAGES/spyder.mo und spyder-3.0.2+dfsg1/spyder/locale/ja/LC_MESSAGES/spyder.mo sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/locale/pt_BR/LC_MESSAGES/spyder.mo und spyder-3.0.2+dfsg1/spyder/locale/pt_BR/LC_MESSAGES/spyder.mo sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/locale/ru/LC_MESSAGES/spyder.mo und spyder-3.0.2+dfsg1/spyder/locale/ru/LC_MESSAGES/spyder.mo sind verschieden. diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/mac_stylesheet.qss spyder-3.0.2+dfsg1/spyder/mac_stylesheet.qss --- spyder-2.3.8+dfsg1/spyder/mac_stylesheet.qss 2015-08-24 21:09:44.000000000 +0200 +++ spyder-3.0.2+dfsg1/spyder/mac_stylesheet.qss 1970-01-01 01:00:00.000000000 +0100 @@ -1,124 +0,0 @@ -/* -* Qt Stylesheet for MacOS X -* Copyright (c) 2015- The Spyder Development Team -*/ - - -/* ---------------- Dock widget and QSplitter separators --------------- */ - -QMainWindow::separator { - width: 3px; - height: 3px; - border: 1px solid lightgrey; - border-radius: 1px; -} - -QMainWindow::separator:hover { - background: darkgrey; -} - -QToolButton { - border: none; -} - -QSplitter::handle:horizontal { - border: 1px solid darkgrey; - width: 2px; -} - -QSplitter::handle:vertical { - border: 1px solid darkgrey; - height: 2px; -} - -QSplitter::handle:pressed { - background: darkgrey; -} - - -/* ----------------- Tabs ------------------ */ - -QWidget#tab-container { - background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, - stop: 0 #b1b1b1, stop: 0.07 #b3b3b3, - stop: 0.33 #b3b3b3, stop: 0.4 #b0b0b0, - stop: 0.47 #b3b3b3, stop: 1.0 #b2b2b2); -} - -QTabWidget::pane#plugin-tab { - border-top: 1px solid qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, - stop: 0 #b1b1b1, stop: 0.07 #b3b3b3, - stop: 0.33 #b3b3b3, stop: 0.4 #b0b0b0, - stop: 0.47 #b3b3b3, stop: 1.0 #b2b2b2); - border-bottom: 0px; - border-left: 0px; - border-right: 0px; -} - -QTabWidget::tab-bar#plugin-tab { - left: 5px; -} - -QTabBar::tab#plugin-tab { - background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, - stop: 0 #b1b1b1, stop: 0.07 #b3b3b3, - stop: 0.33 #b3b3b3, stop: 0.4 #b0b0b0, - stop: 0.47 #b3b3b3, stop: 1.0 #b2b2b2); - border: 1px solid #787878; - border-top-color: transparent; - border-bottom-color: transparent; - margin-left: -1px; - margin-right: -1px; - min-width: 15ex; - padding: 3px; -} - -QTabBar::tab:selected#plugin-tab { - background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, - stop: 0 #dfdfdf, stop: 0.1 #dddddd, - stop: 0.12 #dfdfdf, stop: 0.22 #e0e0e0, - stop: 0.33 #dedede, stop: 0.47 #dedede, - stop: 0.49 #e0e0e0, stop: 0.59 #dddddd, - stop: 0.61 #dfdfdf, stop: 0.73 #dedede, - stop: 0.80 #e0e0e0, stop: 1.0 #dedede); - border: 1px solid #787878; - border-top: 0px; - border-top-color: transparent; - border-bottom-left-radius: 3px; - border-bottom-right-radius: 3px; -} - -QTabBar::tab:first#plugin-tab { - margin-left: 0; -} - -QTabBar::tab:last#plugin-tab { - margin-right: 0; -} - -QTabBar::tab:only-one#plugin-tab { - margin: 0; -} - -QTabBar::scroller#plugin-tab { - width: 22px; -} - -QTabBar#plugin-tab QToolButton::left-arrow { - background: lightgrey; - border-right: 1px solid darkgrey; - image: url(spyderlib/images/chevron-left.png); -} - -QTabBar#plugin-tab QToolButton::right-arrow { - background: lightgrey; - image: url(spyderlib/images/chevron-right.png); -} - - -/* ------------------ Dock widgets ------------------- */ - -QDockWidget::close-button, QDockWidget::float-button { - padding: 0px; - margin: 2px; -} diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/otherplugins.py spyder-3.0.2+dfsg1/spyder/otherplugins.py --- spyder-2.3.8+dfsg1/spyder/otherplugins.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/otherplugins.py 2016-10-25 02:05:23.000000000 +0200 @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- # -# Copyright © 2011 Pierre Raybaut +# Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) +# (see spyder/__init__.py for details) """ Spyder third-party plugins configuration management @@ -14,46 +14,107 @@ import traceback # Local imports -from spyderlib.utils import programs +from spyder.config.base import get_conf_path +from spyder.py3compat import PY2 +if PY2: + import imp +else: + import importlib -# Calculate path to `spyderplugins` package, where Spyder looks for all 3rd -# party plugin modules -PLUGIN_PATH = None -if programs.is_module_installed("spyderplugins"): - import spyderplugins - PLUGIN_PATH = osp.abspath(spyderplugins.__path__[0]) - if not osp.isdir(PLUGIN_PATH): - # py2exe/cx_Freeze distribution: ignoring extra plugins - PLUGIN_PATH = None - - -def get_spyderplugins(prefix, extension): - """Scan directory of `spyderplugins` package and - return the list of module names matching *prefix* and *extension*""" - plist = [] - if PLUGIN_PATH is not None: - for name in os.listdir(PLUGIN_PATH): - modname, ext = osp.splitext(name) - if prefix is not None and not name.startswith(prefix): - continue - if extension is not None and ext != extension: - continue - plist.append(modname) - return plist - - -def get_spyderplugins_mods(prefix, extension): - """Import modules that match *prefix* and *extension* from - `spyderplugins` package and return the list""" - modlist = [] - for modname in get_spyderplugins(prefix, extension): - name = 'spyderplugins.%s' % modname - try: - __import__(name) - modlist.append(sys.modules[name]) - except Exception: - sys.stderr.write( - "ERROR: 3rd party plugin import failed for `%s`\n" % modname) - traceback.print_exc(file=sys.stderr) + +USER_PLUGIN_DIR = "plugins" +PLUGIN_PREFIX = "spyder_" +IO_PREFIX = PLUGIN_PREFIX + "io_" + + +def get_spyderplugins_mods(io=False): + """Import modules from plugins package and return the list""" + # Create user directory + user_plugin_path = osp.join(get_conf_path(), USER_PLUGIN_DIR) + if not osp.isdir(user_plugin_path): + os.makedirs(user_plugin_path) + + modlist, modnames = [], [] + + # The user plugins directory is given the priority when looking for modules + for plugin_path in [user_plugin_path] + sys.path: + _get_spyderplugins(plugin_path, io, modnames, modlist) return modlist + + +def _get_spyderplugins(plugin_path, is_io, modnames, modlist): + """Scan the directory `plugin_path` for plugin packages and loads them.""" + if not osp.isdir(plugin_path): + return + + for name in os.listdir(plugin_path): + if is_io and not name.startswith(IO_PREFIX): + continue + if not name.startswith(PLUGIN_PREFIX) or name.startswith(IO_PREFIX): + continue + + # Import the plugin + _import_plugin(name, plugin_path, modnames, modlist) + + +def _import_plugin(module_name, plugin_path, modnames, modlist): + """Import the plugin `module_name` from `plugin_path`, add it to `modlist` + and adds its name to `modnames`. + """ + if module_name in modnames: + return + try: + # First add a mock module with the LOCALEPATH attribute so that the + # helper method can find the locale on import + mock = _ModuleMock() + mock.LOCALEPATH = osp.join(plugin_path, module_name, 'locale') + sys.modules[module_name] = mock + + if osp.isdir(osp.join(plugin_path, module_name)): + module = _import_module_from_path(module_name, plugin_path) + else: + module = None + + # Then restore the actual loaded module instead of the mock + if module: + sys.modules[module_name] = module + modlist.append(module) + modnames.append(module_name) + except Exception: + sys.stderr.write("ERROR: 3rd party plugin import failed for " + "`{0}`\n".format(module_name)) + traceback.print_exc(file=sys.stderr) + + +def _import_module_from_path(module_name, plugin_path): + """Imports `module_name` from `plugin_path`. + + Return None if no module is found. + """ + module = None + if PY2: + info = imp.find_module(module_name, [plugin_path]) + if info: + module = imp.load_module(module_name, *info) + elif sys.version_info[0:2] <= (3, 3): + loader = importlib.machinery.PathFinder.find_module( + module_name, + [plugin_path]) + if loader: + module = loader.load_module(module_name) + else: # Python 3.4+ + spec = importlib.machinery.PathFinder.find_spec( + module_name, + [plugin_path]) + if spec: + module = spec.loader.load_module(module_name) + return module + + +class _ModuleMock(): + """This mock module is added to sys.modules on plugin load to add the + location of the LOCALEDATA so that the module loads succesfully. + Once loaded the module is replaced by the actual loaded module object. + """ + pass diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/pil_patch.py spyder-3.0.2+dfsg1/spyder/pil_patch.py --- spyder-2.3.8+dfsg1/spyder/pil_patch.py 2015-08-24 21:09:44.000000000 +0200 +++ spyder-3.0.2+dfsg1/spyder/pil_patch.py 2016-10-25 02:05:23.000000000 +0200 @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- # -# Copyright © 2011 Pierre Raybaut +# Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) +# (see spyder/__init__.py for details) """ Patching PIL (Python Imaging Library) to avoid triggering the error: diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/plugins/configdialog.py spyder-3.0.2+dfsg1/spyder/plugins/configdialog.py --- spyder-2.3.8+dfsg1/spyder/plugins/configdialog.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/plugins/configdialog.py 2016-11-19 01:31:58.000000000 +0100 @@ -1,34 +1,42 @@ # -*- coding: utf-8 -*- # -# Copyright © 2009-2010 Pierre Raybaut +# Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) +# (see spyder/__init__.py for details) -"""Configuration dialog / Preferences""" +""" +Configuration dialog / Preferences. +""" +# Standard library imports import os.path as osp -from spyderlib.baseconfig import _, running_in_mac_app -from spyderlib.config import CONF, is_gtk_desktop -from spyderlib.guiconfig import (CUSTOM_COLOR_SCHEME_NAME, - set_default_color_scheme) -from spyderlib.utils.qthelpers import get_icon, get_std_icon -from spyderlib.userconfig import NoDefault -from spyderlib.widgets.colors import ColorLayout -from spyderlib.widgets.sourcecode import syntaxhighlighters as sh - -from spyderlib.qt.QtGui import (QWidget, QDialog, QListWidget, QListWidgetItem, - QVBoxLayout, QStackedWidget, QListView, - QHBoxLayout, QDialogButtonBox, QCheckBox, - QMessageBox, QLabel, QLineEdit, QSpinBox, - QPushButton, QFontComboBox, QGroupBox, - QComboBox, QColor, QGridLayout, QTabWidget, - QRadioButton, QButtonGroup, QSplitter, - QStyleFactory, QScrollArea, QDoubleSpinBox) -from spyderlib.qt.QtCore import Qt, QSize, SIGNAL, SLOT, Slot -from spyderlib.qt.compat import (to_qvariant, from_qvariant, - getexistingdirectory, getopenfilename) -from spyderlib.py3compat import to_text_string, is_text_string, getcwd +# Third party imports +from qtpy import API +from qtpy.compat import (getexistingdirectory, getopenfilename, from_qvariant, + to_qvariant) +from qtpy.QtCore import QSize, Qt, Signal, Slot +from qtpy.QtGui import QColor +from qtpy.QtWidgets import (QButtonGroup, QCheckBox, QComboBox, QDialog, + QDialogButtonBox, QDoubleSpinBox, QFontComboBox, + QGridLayout, QGroupBox, QHBoxLayout, QLabel, + QLineEdit, QListView, QListWidget, QListWidgetItem, + QMessageBox, QPushButton, QRadioButton, + QScrollArea, QSpinBox, QSplitter, QStackedWidget, + QStyleFactory, QTabWidget, QVBoxLayout, QWidget) + +# Local imports +from spyder.config.base import (_, LANGUAGE_CODES, load_lang_conf, + running_in_mac_app, save_lang_conf) +from spyder.config.gui import get_font, set_font +from spyder.config.main import CONF +from spyder.config.user import NoDefault +from spyder.config.utils import is_gtk_desktop +from spyder.py3compat import to_text_string, is_text_string, getcwd +from spyder.utils import icon_manager as ima +from spyder.utils import syntaxhighlighters +from spyder.widgets.colors import ColorLayout +from spyder.widgets.sourcecode.codeeditor import CodeEditor class ConfigAccessMixin(object): @@ -45,6 +53,10 @@ class ConfigPage(QWidget): """Base class for configuration page in Preferences""" + # Signals + apply_button_enabled = Signal(bool) + show_this_page = Signal() + def __init__(self, parent, apply_callback=None): QWidget.__init__(self, parent) self.apply_callback = apply_callback @@ -73,8 +85,8 @@ def set_modified(self, state): self.is_modified = state - self.emit(SIGNAL("apply_button_enabled(bool)"), state) - + self.apply_button_enabled.emit(state) + def is_valid(self): """Return True if all widget contents are valid""" raise NotImplementedError @@ -85,8 +97,21 @@ self.save_to_conf() if self.apply_callback is not None: self.apply_callback() + + # Since the language cannot be retrieved by CONF and the language + # is needed before loading CONF, this is an extra method needed to + # ensure that when changes are applied, they are copied to a + # specific file storing the language value. This only applies to + # the main section config. + if self.CONF_SECTION == u'main': + self._save_lang() + + for restart_option in self.restart_options: + if restart_option in self.changed_options: + self.prompt_restart_required() + break # Ensure a single popup is displayed self.set_modified(False) - + def load_from_conf(self): """Load settings from configuration file""" raise NotImplementedError @@ -98,40 +123,44 @@ class ConfigDialog(QDialog): """Spyder configuration ('Preferences') dialog box""" + + # Signals + check_settings = Signal() + size_change = Signal(QSize) + def __init__(self, parent=None): QDialog.__init__(self, parent) - + + self.main = parent + + # Widgets + self.pages_widget = QStackedWidget() + self.contents_widget = QListWidget() + self.button_reset = QPushButton(_('Reset to defaults')) + + bbox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Apply | + QDialogButtonBox.Cancel) + self.apply_btn = bbox.button(QDialogButtonBox.Apply) + + # Widgets setup # Destroying the C++ object right after closing the dialog box, # otherwise it may be garbage-collected in another QThread # (e.g. the editor's analysis thread in Spyder), thus leading to # a segmentation fault on UNIX or an application crash on Windows self.setAttribute(Qt.WA_DeleteOnClose) - - self.contents_widget = QListWidget() + self.setWindowTitle(_('Preferences')) + self.setWindowIcon(ima.icon('configure')) self.contents_widget.setMovement(QListView.Static) self.contents_widget.setSpacing(1) - - bbox = QDialogButtonBox(QDialogButtonBox.Ok|QDialogButtonBox.Apply - |QDialogButtonBox.Cancel) - self.apply_btn = bbox.button(QDialogButtonBox.Apply) - self.connect(bbox, SIGNAL("accepted()"), SLOT("accept()")) - self.connect(bbox, SIGNAL("rejected()"), SLOT("reject()")) - self.connect(bbox, SIGNAL("clicked(QAbstractButton*)"), - self.button_clicked) - - self.pages_widget = QStackedWidget() - self.connect(self.pages_widget, SIGNAL("currentChanged(int)"), - self.current_page_changed) - - self.connect(self.contents_widget, SIGNAL("currentRowChanged(int)"), - self.pages_widget.setCurrentIndex) self.contents_widget.setCurrentRow(0) + # Layout hsplitter = QSplitter() hsplitter.addWidget(self.contents_widget) hsplitter.addWidget(self.pages_widget) btnlayout = QHBoxLayout() + btnlayout.addWidget(self.button_reset) btnlayout.addStretch(1) btnlayout.addWidget(bbox) @@ -141,9 +170,19 @@ self.setLayout(vlayout) - self.setWindowTitle(_("Preferences")) - self.setWindowIcon(get_icon("configure.png")) - + # Signals and slots + if self.main: + self.button_reset.clicked.connect(self.main.reset_spyder) + self.pages_widget.currentChanged.connect(self.current_page_changed) + self.contents_widget.currentRowChanged.connect( + self.pages_widget.setCurrentIndex) + bbox.accepted.connect(self.accept) + bbox.rejected.connect(self.reject) + bbox.clicked.connect(self.button_clicked) + + # Ensures that the config is present on spyder first run + CONF.set('main', 'interface_language', load_lang_conf()) + def get_current_index(self): """Return current page index""" return self.contents_widget.currentRow() @@ -159,7 +198,8 @@ else: widget = self.pages_widget.widget(index) return widget.widget() - + + @Slot() def accept(self): """Reimplement Qt method""" for index in range(self.pages_widget.count()): @@ -183,18 +223,19 @@ self.apply_btn.setEnabled(widget.is_modified) def add_page(self, widget): - self.connect(self, SIGNAL('check_settings()'), widget.check_settings) - self.connect(widget, SIGNAL('show_this_page()'), - lambda row=self.contents_widget.count(): - self.contents_widget.setCurrentRow(row)) - self.connect(widget, SIGNAL("apply_button_enabled(bool)"), - self.apply_btn.setEnabled) + self.check_settings.connect(widget.check_settings) + widget.show_this_page.connect(lambda row=self.contents_widget.count(): + self.contents_widget.setCurrentRow(row)) + widget.apply_button_enabled.connect(self.apply_btn.setEnabled) scrollarea = QScrollArea(self) scrollarea.setWidgetResizable(True) scrollarea.setWidget(widget) self.pages_widget.addWidget(scrollarea) item = QListWidgetItem(self.contents_widget) - item.setIcon(widget.get_icon()) + try: + item.setIcon(widget.get_icon()) + except TypeError: + pass item.setText(widget.get_name()) item.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled) item.setSizeHint(QSize(0, 25)) @@ -202,7 +243,7 @@ def check_all_settings(self): """This method is called to check all configuration page settings after configuration dialog has been shown""" - self.emit(SIGNAL('check_settings()')) + self.check_settings.emit() def resizeEvent(self, event): """ @@ -210,7 +251,7 @@ main application """ QDialog.resizeEvent(self, event) - self.emit(SIGNAL("size_change(QSize)"), self.size()) + self.size_change.emit(self.size()) class SpyderConfigPage(ConfigPage, ConfigAccessMixin): @@ -231,6 +272,7 @@ self.coloredits = {} self.scedits = {} self.changed_options = set() + self.restart_options = dict() # Dict to store name and localized text self.default_button_group = None def apply_settings(self, options): @@ -263,36 +305,40 @@ """Load settings from configuration file""" for checkbox, (option, default) in list(self.checkboxes.items()): checkbox.setChecked(self.get_option(option, default)) - self.connect(checkbox, SIGNAL("clicked(bool)"), - lambda _foo, opt=option: self.has_been_modified(opt)) + # QAbstractButton works differently for PySide and PyQt + if not API == 'pyside': + checkbox.clicked.connect(lambda _foo, opt=option: + self.has_been_modified(opt)) + else: + checkbox.clicked.connect(lambda opt=option: + self.has_been_modified(opt)) for radiobutton, (option, default) in list(self.radiobuttons.items()): radiobutton.setChecked(self.get_option(option, default)) - self.connect(radiobutton, SIGNAL("toggled(bool)"), - lambda _foo, opt=option: self.has_been_modified(opt)) + radiobutton.toggled.connect(lambda _foo, opt=option: + self.has_been_modified(opt)) for lineedit, (option, default) in list(self.lineedits.items()): lineedit.setText(self.get_option(option, default)) - self.connect(lineedit, SIGNAL("textChanged(QString)"), - lambda _foo, opt=option: self.has_been_modified(opt)) + lineedit.textChanged.connect(lambda _foo, opt=option: + self.has_been_modified(opt)) for spinbox, (option, default) in list(self.spinboxes.items()): spinbox.setValue(self.get_option(option, default)) - if type(spinbox) is QSpinBox: - self.connect(spinbox, SIGNAL('valueChanged(int)'), - lambda _foo, opt=option: self.has_been_modified(opt)) - else: - self.connect(spinbox, SIGNAL('valueChanged(double)'), - lambda _foo, opt=option: self.has_been_modified(opt)) + spinbox.valueChanged.connect(lambda _foo, opt=option: + self.has_been_modified(opt)) for combobox, (option, default) in list(self.comboboxes.items()): value = self.get_option(option, default) for index in range(combobox.count()): data = from_qvariant(combobox.itemData(index), to_text_string) - # For PyQt API v2, it is necessary to convert `data` to - # unicode in case the original type was not a string, like an - # integer for example (see spyderlib.qt.compat.from_qvariant): + # For PyQt API v2, it is necessary to convert `data` to + # unicode in case the original type was not a string, like an + # integer for example (see qtpy.compat.from_qvariant): if to_text_string(data) == to_text_string(value): break combobox.setCurrentIndex(index) - self.connect(combobox, SIGNAL('currentIndexChanged(int)'), - lambda _foo, opt=option: self.has_been_modified(opt)) + combobox.currentIndexChanged.connect(lambda _foo, opt=option: + self.has_been_modified(opt)) + if combobox.restart_required: + self.restart_options[option] = combobox.label_text + for (fontbox, sizebox), option in list(self.fontboxes.items()): font = self.get_font(option) fontbox.setCurrentFont(font) @@ -301,19 +347,24 @@ property = 'plugin_font' else: property = option - self.connect(fontbox, SIGNAL('currentIndexChanged(int)'), - lambda _foo, opt=property: self.has_been_modified(opt)) - self.connect(sizebox, SIGNAL('valueChanged(int)'), - lambda _foo, opt=property: self.has_been_modified(opt)) + fontbox.currentIndexChanged.connect(lambda _foo, opt=property: + self.has_been_modified(opt)) + sizebox.valueChanged.connect(lambda _foo, opt=property: + self.has_been_modified(opt)) for clayout, (option, default) in list(self.coloredits.items()): property = to_qvariant(option) edit = clayout.lineedit btn = clayout.colorbtn edit.setText(self.get_option(option, default)) - self.connect(btn, SIGNAL('clicked()'), - lambda opt=option: self.has_been_modified(opt)) - self.connect(edit, SIGNAL("textChanged(QString)"), - lambda _foo, opt=option: self.has_been_modified(opt)) + # QAbstractButton works differently for PySide and PyQt + if not API == 'pyside': + btn.clicked.connect(lambda _foo, opt=option: + self.has_been_modified(opt)) + else: + btn.clicked.connect(lambda opt=option: + self.has_been_modified(opt)) + edit.textChanged.connect(lambda _foo, opt=option: + self.has_been_modified(opt)) for (clayout, cb_bold, cb_italic ), (option, default) in list(self.scedits.items()): edit = clayout.lineedit @@ -322,15 +373,24 @@ edit.setText(color) cb_bold.setChecked(bold) cb_italic.setChecked(italic) - self.connect(btn, SIGNAL('clicked()'), - lambda opt=option: self.has_been_modified(opt)) - self.connect(edit, SIGNAL("textChanged(QString)"), - lambda _foo, opt=option: self.has_been_modified(opt)) - self.connect(cb_bold, SIGNAL("clicked(bool)"), - lambda _foo, opt=option: self.has_been_modified(opt)) - self.connect(cb_italic, SIGNAL("clicked(bool)"), - lambda _foo, opt=option: self.has_been_modified(opt)) - + edit.textChanged.connect(lambda _foo, opt=option: + self.has_been_modified(opt)) + # QAbstractButton works differently for PySide and PyQt + if not API == 'pyside': + btn.clicked.connect(lambda _foo, opt=option: + self.has_been_modified(opt)) + cb_bold.clicked.connect(lambda _foo, opt=option: + self.has_been_modified(opt)) + cb_italic.clicked.connect(lambda _foo, opt=option: + self.has_been_modified(opt)) + else: + btn.clicked.connect(lambda opt=option: + self.has_been_modified(opt)) + cb_bold.clicked.connect(lambda opt=option: + self.has_been_modified(opt)) + cb_italic.clicked.connect(lambda opt=option: + self.has_been_modified(opt)) + def save_to_conf(self): """Save settings to configuration file""" for checkbox, (option, _default) in list(self.checkboxes.items()): @@ -360,7 +420,7 @@ def has_been_modified(self, option): self.set_modified(True) self.changed_options.add(option) - + def create_checkbox(self, text, option, default=NoDefault, tip=None, msg_warning=None, msg_info=None, msg_if_enabled=False): @@ -369,7 +429,7 @@ checkbox.setToolTip(tip) self.checkboxes[checkbox] = (option, default) if msg_warning is not None or msg_info is not None: - def show_message(is_checked): + def show_message(is_checked=False): if is_checked or not msg_if_enabled: if msg_warning is not None: QMessageBox.warning(self, self.get_name(), @@ -377,7 +437,7 @@ if msg_info is not None: QMessageBox.information(self, self.get_name(), msg_info, QMessageBox.Ok) - self.connect(checkbox, SIGNAL("clicked(bool)"), show_message) + checkbox.clicked.connect(show_message) return checkbox def create_radiobutton(self, text, option, default=NoDefault, @@ -401,7 +461,7 @@ if msg_info is not None: QMessageBox.information(self, self.get_name(), msg_info, QMessageBox.Ok) - self.connect(radiobutton, SIGNAL("toggled(bool)"), show_message) + radiobutton.toggled.connect(show_message) return radiobutton def create_lineedit(self, text, option, default=NoDefault, @@ -417,6 +477,8 @@ edit.setToolTip(tip) self.lineedits[edit] = (option, default) widget = QWidget(self) + widget.label = label + widget.textbox = edit widget.setLayout(layout) return widget @@ -428,10 +490,9 @@ break msg = _("Invalid directory path") self.validate_data[edit] = (osp.isdir, msg) - browse_btn = QPushButton(get_std_icon('DirOpenIcon'), "", self) + browse_btn = QPushButton(ima.icon('DirOpenIcon'), '', self) browse_btn.setToolTip(_("Select directory")) - self.connect(browse_btn, SIGNAL("clicked()"), - lambda: self.select_directory(edit)) + browse_btn.clicked.connect(lambda: self.select_directory(edit)) layout = QHBoxLayout() layout.addWidget(widget) layout.addWidget(browse_btn) @@ -457,12 +518,11 @@ for edit in self.lineedits: if widget.isAncestorOf(edit): break - msg = _("Invalid file path") + msg = _('Invalid file path') self.validate_data[edit] = (osp.isfile, msg) - browse_btn = QPushButton(get_std_icon('FileIcon'), "", self) + browse_btn = QPushButton(ima.icon('FileIcon'), '', self) browse_btn.setToolTip(_("Select file")) - self.connect(browse_btn, SIGNAL("clicked()"), - lambda: self.select_file(edit, filters)) + browse_btn.clicked.connect(lambda: self.select_file(edit, filters)) layout = QHBoxLayout() layout.addWidget(widget) layout.addWidget(browse_btn) @@ -485,12 +545,15 @@ def create_spinbox(self, prefix, suffix, option, default=NoDefault, min_=None, max_=None, step=None, tip=None): + widget = QWidget(self) if prefix: plabel = QLabel(prefix) + widget.plabel = plabel else: plabel = None if suffix: slabel = QLabel(suffix) + widget.slabel = slabel else: slabel = None if step is not None: @@ -515,7 +578,7 @@ layout.addWidget(subwidget) layout.addStretch(1) layout.setContentsMargins(0, 0, 0, 0) - widget = QWidget(self) + widget.spinbox = spinbox widget.setLayout(layout) return widget @@ -546,10 +609,10 @@ if tip is not None: clayout.setToolTip(tip) cb_bold = QCheckBox() - cb_bold.setIcon(get_icon("bold.png")) + cb_bold.setIcon(ima.icon('bold')) cb_bold.setToolTip(_("Bold")) cb_italic = QCheckBox() - cb_italic.setIcon(get_icon("italic.png")) + cb_italic.setIcon(ima.icon('italic')) cb_italic.setToolTip(_("Italic")) self.scedits[(clayout, cb_bold, cb_italic)] = (option, default) if without_layout: @@ -567,52 +630,84 @@ return widget def create_combobox(self, text, choices, option, default=NoDefault, - tip=None): + tip=None, restart=False): """choices: couples (name, key)""" label = QLabel(text) combobox = QComboBox() if tip is not None: combobox.setToolTip(tip) for name, key in choices: - combobox.addItem(name, to_qvariant(key)) + if not (name is None and key is None): + combobox.addItem(name, to_qvariant(key)) + # Insert separators + count = 0 + for index, item in enumerate(choices): + name, key = item + if name is None and key is None: + combobox.insertSeparator(index + count) + count += 1 self.comboboxes[combobox] = (option, default) layout = QHBoxLayout() - for subwidget in (label, combobox): - layout.addWidget(subwidget) + layout.addWidget(label) + layout.addWidget(combobox) layout.addStretch(1) layout.setContentsMargins(0, 0, 0, 0) widget = QWidget(self) + widget.label = label + widget.combobox = combobox widget.setLayout(layout) + combobox.restart_required = restart + combobox.label_text = text return widget - def create_fontgroup(self, option=None, text=None, - tip=None, fontfilters=None): + def create_fontgroup(self, option=None, text=None, title=None, + tip=None, fontfilters=None, without_group=False): """Option=None -> setting plugin font""" - fontlabel = QLabel(_("Font: ")) + + if title: + fontlabel = QLabel(title) + else: + fontlabel = QLabel(_("Font: ")) fontbox = QFontComboBox() + if fontfilters is not None: fontbox.setFontFilters(fontfilters) + sizelabel = QLabel(" "+_("Size: ")) sizebox = QSpinBox() sizebox.setRange(7, 100) self.fontboxes[(fontbox, sizebox)] = option layout = QHBoxLayout() + for subwidget in (fontlabel, fontbox, sizelabel, sizebox): layout.addWidget(subwidget) layout.addStretch(1) - if text is None: - text = _("Font style") - group = QGroupBox(text) - group.setLayout(layout) - if tip is not None: - group.setToolTip(tip) - return group - + + widget = QWidget(self) + widget.fontlabel = fontlabel + widget.sizelabel = sizelabel + widget.fontbox = fontbox + widget.sizebox = sizebox + widget.setLayout(layout) + + if not without_group: + if text is None: + text = _("Font style") + + group = QGroupBox(text) + group.setLayout(layout) + + if tip is not None: + group.setToolTip(tip) + + return group + else: + return widget + def create_button(self, text, callback): btn = QPushButton(text) - self.connect(btn, SIGNAL('clicked()'), callback) - self.connect(btn, SIGNAL('clicked()'), - lambda opt='': self.has_been_modified(opt)) + btn.clicked.connect(callback) + btn.clicked.connect(lambda checked=False, opt='': self.has_been_modified(opt)) return btn def create_tab(self, *widgets): @@ -626,6 +721,23 @@ return widget +class PluginConfigPage(SpyderConfigPage): + """Plugin configuration dialog box page widget""" + def __init__(self, plugin, parent): + self.plugin = plugin + self.get_option = plugin.get_option + self.set_option = plugin.set_option + self.get_font = plugin.get_plugin_font + self.apply_settings = plugin.apply_plugin_settings + SpyderConfigPage.__init__(self, parent) + + def get_name(self): + return self.plugin.get_plugin_title() + + def get_icon(self): + return self.plugin.get_plugin_icon() + + class GeneralConfigPage(SpyderConfigPage): """Config page that maintains reference to main Spyder window and allows to specify page name and icon declaratively @@ -645,22 +757,82 @@ def get_icon(self): """Loads page icon named by self.ICON""" - return get_icon(self.ICON) + return self.ICON def apply_settings(self, options): raise NotImplementedError + def prompt_restart_required(self): + """Prompt the user with a request to restart.""" + restart_opts = self.restart_options + changed_opts = self.changed_options + options = [restart_opts[o] for o in changed_opts if o in restart_opts] + + if len(options) == 1: + msg_start = _("Spyder needs to restart to change the following " + "setting:") + else: + msg_start = _("Spyder needs to restart to change the following " + "settings:") + msg_end = _("Do you wish to restart now?") + + msg_options = u"" + for option in options: + msg_options += u"

  • {0}
  • ".format(option) + + msg_title = _("Information") + msg = u"{0}
      {1}

    {2}".format(msg_start, msg_options, msg_end) + answer = QMessageBox.information(self, msg_title, msg, + QMessageBox.Yes | QMessageBox.No) + if answer == QMessageBox.Yes: + self.restart() + + def restart(self): + """Restart Spyder.""" + self.main.restart() + class MainConfigPage(GeneralConfigPage): CONF_SECTION = "main" - NAME = _("General") - ICON = "genprefs.png" - + def setup_page(self): + self.ICON = ima.icon('genprefs') newcb = self.create_checkbox # --- Interface + general_group = QGroupBox(_("General")) + languages = LANGUAGE_CODES.items() + language_choices = sorted([(val, key) for key, val in languages]) + language_combo = self.create_combobox(_('Language'), language_choices, + 'interface_language', + restart=True) + single_instance_box = newcb(_("Use a single instance"), + 'single_instance', + tip=_("Set this to open external
    " + "Python files in an already running " + "instance (Requires a restart)")) + prompt_box = newcb(_("Prompt when exiting"), 'prompt_on_exit') + popup_console_box = newcb(_("Pop up internal console when internal " + "errors appear"), + 'show_internal_console_if_traceback') + check_updates = newcb(_("Check for updates on startup"), + 'check_updates_on_startup') + + # Decide if it's possible to activate or not single instance mode + if running_in_mac_app(): + self.set_option("single_instance", True) + single_instance_box.setEnabled(False) + + general_layout = QVBoxLayout() + general_layout.addWidget(language_combo) + general_layout.addWidget(single_instance_box) + general_layout.addWidget(prompt_box) + general_layout.addWidget(popup_console_box) + general_layout.addWidget(check_updates) + general_group.setLayout(general_layout) + + # --- Theme interface_group = QGroupBox(_("Interface")) styles = [str(txt) for txt in list(QStyleFactory.keys())] # Don't offer users the possibility to change to a different @@ -673,169 +845,619 @@ 'windows_style', default=self.main.default_style) - single_instance_box = newcb(_("Use a single instance"), - 'single_instance', - tip=_("Set this to open external
    " - "Python files in an already running " - "instance (Requires a restart)")) - vertdock_box = newcb(_("Vertical dockwidget title bars"), + themes = ['Spyder 2', 'Spyder 3'] + icon_choices = list(zip(themes, [theme.lower() for theme in themes])) + icons_combo = self.create_combobox(_('Icon theme'), icon_choices, + 'icon_theme', restart=True) + + + vertdock_box = newcb(_("Vertical title bars in panes"), 'vertical_dockwidget_titlebars') - verttabs_box = newcb(_("Vertical dockwidget tabs"), + verttabs_box = newcb(_("Vertical tabs in panes"), 'vertical_tabs') - animated_box = newcb(_("Animated toolbars and dockwidgets"), + animated_box = newcb(_("Animated toolbars and panes"), 'animated_docks') tear_off_box = newcb(_("Tear off menus"), 'tear_off_menus', tip=_("Set this to detach any
    " "menu from the main window")) - margin_box = newcb(_("Custom dockwidget margin:"), + high_dpi_scaling_box = newcb(_("Enable high DPI scaling"), + 'high_dpi_scaling', + tip=_("Set this for high DPI displays")) + margin_box = newcb(_("Custom margin for panes:"), 'use_custom_margin') - margin_spin = self.create_spinbox("", "pixels", 'custom_margin', + margin_spin = self.create_spinbox("", _("pixels"), 'custom_margin', 0, 0, 30) - self.connect(margin_box, SIGNAL("toggled(bool)"), - margin_spin.setEnabled) + margin_box.toggled.connect(margin_spin.setEnabled) margin_spin.setEnabled(self.get_option('use_custom_margin')) margins_layout = QHBoxLayout() margins_layout.addWidget(margin_box) margins_layout.addWidget(margin_spin) - # Decide if it's possible to activate or not singie instance mode - if running_in_mac_app(): - self.set_option("single_instance", True) - single_instance_box.setEnabled(False) + # Layout interface + comboboxes_layout = QHBoxLayout() + cbs_layout = QGridLayout() + cbs_layout.addWidget(style_combo.label, 0, 0) + cbs_layout.addWidget(style_combo.combobox, 0, 1) + cbs_layout.addWidget(icons_combo.label, 1, 0) + cbs_layout.addWidget(icons_combo.combobox, 1, 1) + comboboxes_layout.addLayout(cbs_layout) + comboboxes_layout.addStretch(1) interface_layout = QVBoxLayout() - interface_layout.addWidget(style_combo) - interface_layout.addWidget(single_instance_box) + interface_layout.addLayout(comboboxes_layout) interface_layout.addWidget(vertdock_box) interface_layout.addWidget(verttabs_box) interface_layout.addWidget(animated_box) interface_layout.addWidget(tear_off_box) + interface_layout.addWidget(high_dpi_scaling_box) interface_layout.addLayout(margins_layout) interface_group.setLayout(interface_layout) # --- Status bar sbar_group = QGroupBox(_("Status bar")) + show_status_bar = newcb(_("Show status bar"), 'show_status_bar') + memory_box = newcb(_("Show memory usage every"), 'memory_usage/enable', tip=self.main.mem_status.toolTip()) - memory_spin = self.create_spinbox("", " ms", 'memory_usage/timeout', + memory_spin = self.create_spinbox("", _(" ms"), 'memory_usage/timeout', min_=100, max_=1000000, step=100) - self.connect(memory_box, SIGNAL("toggled(bool)"), - memory_spin.setEnabled) + memory_box.toggled.connect(memory_spin.setEnabled) memory_spin.setEnabled(self.get_option('memory_usage/enable')) - memory_layout = QHBoxLayout() - memory_layout.addWidget(memory_box) - memory_layout.addWidget(memory_spin) - memory_layout.setEnabled(self.main.mem_status.is_supported()) + memory_box.setEnabled(self.main.mem_status.is_supported()) + memory_spin.setEnabled(self.main.mem_status.is_supported()) + cpu_box = newcb(_("Show CPU usage every"), 'cpu_usage/enable', tip=self.main.cpu_status.toolTip()) - cpu_spin = self.create_spinbox("", " ms", 'cpu_usage/timeout', + cpu_spin = self.create_spinbox("", _(" ms"), 'cpu_usage/timeout', min_=100, max_=1000000, step=100) - self.connect(cpu_box, SIGNAL("toggled(bool)"), cpu_spin.setEnabled) + cpu_box.toggled.connect(cpu_spin.setEnabled) cpu_spin.setEnabled(self.get_option('cpu_usage/enable')) - cpu_layout = QHBoxLayout() - cpu_layout.addWidget(cpu_box) - cpu_layout.addWidget(cpu_spin) - cpu_layout.setEnabled(self.main.cpu_status.is_supported()) + + cpu_box.setEnabled(self.main.cpu_status.is_supported()) + cpu_spin.setEnabled(self.main.cpu_status.is_supported()) + status_bar_o = self.get_option('show_status_bar') + show_status_bar.toggled.connect(memory_box.setEnabled) + show_status_bar.toggled.connect(memory_spin.setEnabled) + show_status_bar.toggled.connect(cpu_box.setEnabled) + show_status_bar.toggled.connect(cpu_spin.setEnabled) + memory_box.setEnabled(status_bar_o) + memory_spin.setEnabled(status_bar_o) + cpu_box.setEnabled(status_bar_o) + cpu_spin.setEnabled(status_bar_o) + + # Layout status bar + cpu_memory_layout = QGridLayout() + cpu_memory_layout.addWidget(memory_box, 0, 0) + cpu_memory_layout.addWidget(memory_spin, 0, 1) + cpu_memory_layout.addWidget(cpu_box, 1, 0) + cpu_memory_layout.addWidget(cpu_spin, 1, 1) + sbar_layout = QVBoxLayout() - sbar_layout.addLayout(memory_layout) - sbar_layout.addLayout(cpu_layout) + sbar_layout.addWidget(show_status_bar) + sbar_layout.addLayout(cpu_memory_layout) sbar_group.setLayout(sbar_layout) - # --- Debugging - debug_group = QGroupBox(_("Debugging")) - popup_console_box = newcb(_("Pop up internal console when internal " - "errors appear"), - 'show_internal_console_if_traceback') - - debug_layout = QVBoxLayout() - debug_layout.addWidget(popup_console_box) - debug_group.setLayout(debug_layout) - + # --- Theme and fonts + plain_text_font = self.create_fontgroup( + option='font', + title=_("Plain text font"), + fontfilters=QFontComboBox.MonospacedFonts, + without_group=True) + + rich_text_font = self.create_fontgroup( + option='rich_font', + title=_("Rich text font"), + without_group=True) + + fonts_group = QGroupBox(_("Fonts")) + fonts_layout = QGridLayout() + fonts_layout.addWidget(plain_text_font.fontlabel, 0, 0) + fonts_layout.addWidget(plain_text_font.fontbox, 0, 1) + fonts_layout.addWidget(plain_text_font.sizelabel, 0, 2) + fonts_layout.addWidget(plain_text_font.sizebox, 0, 3) + fonts_layout.addWidget(rich_text_font.fontlabel, 1, 0) + fonts_layout.addWidget(rich_text_font.fontbox, 1, 1) + fonts_layout.addWidget(rich_text_font.sizelabel, 1, 2) + fonts_layout.addWidget(rich_text_font.sizebox, 1, 3) + fonts_group.setLayout(fonts_layout) + + tabs = QTabWidget() + tabs.addTab(self.create_tab(fonts_group, interface_group), + _("Appearance")) + tabs.addTab(self.create_tab(general_group, sbar_group), + _("Advanced Settings")) + vlayout = QVBoxLayout() - vlayout.addWidget(interface_group) - vlayout.addWidget(sbar_group) - vlayout.addWidget(debug_group) - vlayout.addStretch(1) + vlayout.addWidget(tabs) self.setLayout(vlayout) - + + def get_font(self, option): + """Return global font used in Spyder.""" + return get_font(option=option) + + def set_font(self, font, option): + """Set global font used in Spyder.""" + # Update fonts in all plugins + set_font(font, option=option) + plugins = self.main.widgetlist + self.main.thirdparty_plugins + for plugin in plugins: + plugin.update_font() + def apply_settings(self, options): self.main.apply_settings() + def _save_lang(self): + """ + Get selected language setting and save to language configuration file. + """ + for combobox, (option, _default) in list(self.comboboxes.items()): + if option == 'interface_language': + data = combobox.itemData(combobox.currentIndex()) + value = from_qvariant(data, to_text_string) + break + save_lang_conf(value) + self.set_option('interface_language', value) + class ColorSchemeConfigPage(GeneralConfigPage): CONF_SECTION = "color_schemes" - NAME = _("Syntax coloring") - ICON = "genprefs.png" - + def setup_page(self): - tabs = QTabWidget() + self.ICON = ima.icon('eyedropper') + names = self.get_option("names") - names.pop(names.index(CUSTOM_COLOR_SCHEME_NAME)) - names.insert(0, CUSTOM_COLOR_SCHEME_NAME) - fieldnames = { - "background": _("Background:"), - "currentline": _("Current line:"), - "currentcell": _("Current cell:"), - "occurence": _("Occurence:"), - "ctrlclick": _("Link:"), - "sideareas": _("Side areas:"), - "matched_p": _("Matched parentheses:"), - "unmatched_p": _("Unmatched parentheses:"), - "normal": _("Normal text:"), - "keyword": _("Keyword:"), - "builtin": _("Builtin:"), - "definition": _("Definition:"), - "comment": _("Comment:"), - "string": _("String:"), - "number": _("Number:"), - "instance": _("Instance:"), - } - from spyderlib.widgets.sourcecode import syntaxhighlighters - assert all([key in fieldnames - for key in syntaxhighlighters.COLOR_SCHEME_KEYS]) - for tabname in names: - cs_group = QGroupBox(_("Color scheme")) - cs_layout = QGridLayout() - for row, key in enumerate(syntaxhighlighters.COLOR_SCHEME_KEYS): - option = "%s/%s" % (tabname, key) - value = self.get_option(option) - name = fieldnames[key] - if is_text_string(value): - label, clayout = self.create_coloredit(name, option, - without_layout=True) - label.setAlignment(Qt.AlignRight|Qt.AlignVCenter) - cs_layout.addWidget(label, row+1, 0) - cs_layout.addLayout(clayout, row+1, 1) - else: - label, clayout, cb_bold, cb_italic = self.create_scedit( - name, option, without_layout=True) - label.setAlignment(Qt.AlignRight|Qt.AlignVCenter) - cs_layout.addWidget(label, row+1, 0) - cs_layout.addLayout(clayout, row+1, 1) - cs_layout.addWidget(cb_bold, row+1, 2) - cs_layout.addWidget(cb_italic, row+1, 3) - cs_group.setLayout(cs_layout) - if tabname in sh.COLOR_SCHEME_NAMES: - def_btn = self.create_button(_("Reset to default values"), - lambda: self.reset_to_default(tabname)) - tabs.addTab(self.create_tab(cs_group, def_btn), tabname) - else: - tabs.addTab(self.create_tab(cs_group), tabname) - + try: + names.pop(names.index(u'Custom')) + except ValueError: + pass + custom_names = self.get_option("custom_names", []) + + # Widgets + about_label = QLabel(_("Here you can select the color scheme used in " + "the Editor and all other Spyder plugins.

    " + "You can also edit the color schemes provided " + "by Spyder or create your own ones by using " + "the options provided below.
    ")) + edit_button = QPushButton(_("Edit selected")) + create_button = QPushButton(_("Create new scheme")) + self.delete_button = QPushButton(_("Delete")) + self.preview_editor = CodeEditor(self) + self.stacked_widget = QStackedWidget(self) + self.reset_button = QPushButton(_("Reset")) + self.scheme_editor_dialog = SchemeEditor(parent=self, + stack=self.stacked_widget) + + # Widget setup + self.scheme_choices_dict = {} + about_label.setWordWrap(True) + schemes_combobox_widget = self.create_combobox(_('Scheme:'), + [('', '')], + 'selected') + self.schemes_combobox = schemes_combobox_widget.combobox + + # Layouts vlayout = QVBoxLayout() - vlayout.addWidget(tabs) + + manage_layout = QVBoxLayout() + manage_layout.addWidget(about_label) + + combo_layout = QHBoxLayout() + combo_layout.addWidget(schemes_combobox_widget.label) + combo_layout.addWidget(schemes_combobox_widget.combobox) + + buttons_layout = QVBoxLayout() + buttons_layout.addLayout(combo_layout) + buttons_layout.addWidget(edit_button) + buttons_layout.addWidget(self.reset_button) + buttons_layout.addWidget(self.delete_button) + buttons_layout.addStretch(1) + buttons_layout.addWidget(create_button) + + preview_layout = QVBoxLayout() + preview_layout.addWidget(self.preview_editor) + + buttons_preview_layout = QHBoxLayout() + buttons_preview_layout.addLayout(buttons_layout) + buttons_preview_layout.addLayout(preview_layout) + + manage_layout.addLayout(buttons_preview_layout) + manage_group = QGroupBox(_("Manage color schemes")) + manage_group.setLayout(manage_layout) + + vlayout.addWidget(manage_group) self.setLayout(vlayout) - - @Slot(str) - def reset_to_default(self, name): - set_default_color_scheme(name, replace=True) - self.load_from_conf() - + + # Signals and slots + create_button.clicked.connect(self.create_new_scheme) + edit_button.clicked.connect(self.edit_scheme) + self.reset_button.clicked.connect(self.reset_to_default) + self.delete_button.clicked.connect(self.delete_scheme) + self.schemes_combobox.currentIndexChanged.connect(self.update_preview) + self.schemes_combobox.currentIndexChanged.connect(self.update_buttons) + + # Setup + for name in names: + self.scheme_editor_dialog.add_color_scheme_stack(name) + + for name in custom_names: + self.scheme_editor_dialog.add_color_scheme_stack(name, custom=True) + + self.update_combobox() + self.update_preview() + def apply_settings(self, options): + self.set_option('selected', self.current_scheme) self.main.editor.apply_plugin_settings(['color_scheme_name']) if self.main.historylog is not None: self.main.historylog.apply_plugin_settings(['color_scheme_name']) - if self.main.inspector is not None: - self.main.inspector.apply_plugin_settings(['color_scheme_name']) + if self.main.help is not None: + self.main.help.apply_plugin_settings(['color_scheme_name']) + self.update_combobox() + self.update_preview() + + # Helpers + # ------------------------------------------------------------------------- + @property + def current_scheme_name(self): + return self.schemes_combobox.currentText() + + @property + def current_scheme(self): + return self.scheme_choices_dict[self.current_scheme_name] + + @property + def current_scheme_index(self): + return self.schemes_combobox.currentIndex() + + def update_combobox(self): + """Recreates the combobox contents.""" + index = self.current_scheme_index + self.schemes_combobox.blockSignals(True) + names = self.get_option("names") + try: + names.pop(names.index(u'Custom')) + except ValueError: + pass + custom_names = self.get_option("custom_names", []) + + # Useful for retrieving the actual data + for n in names + custom_names: + self.scheme_choices_dict[self.get_option('{0}/name'.format(n))] = n + + if custom_names: + choices = names + [None] + custom_names + else: + choices = names + + combobox = self.schemes_combobox + combobox.clear() + + for name in choices: + if name is None: + continue + combobox.addItem(self.get_option('{0}/name'.format(name)), name) + + if custom_names: + combobox.insertSeparator(len(names)) + + self.schemes_combobox.blockSignals(False) + self.schemes_combobox.setCurrentIndex(index) + + def update_buttons(self): + """Updates the enable status of delete and reset buttons.""" + current_scheme = self.current_scheme + names = self.get_option("names") + try: + names.pop(names.index(u'Custom')) + except ValueError: + pass + delete_enabled = current_scheme not in names + self.delete_button.setEnabled(delete_enabled) + self.reset_button.setEnabled(not delete_enabled) + + def update_preview(self, index=None, scheme_name=None): + """ + Update the color scheme of the preview editor and adds text. + + Note + ---- + 'index' is needed, because this is triggered by a signal that sends + the selected index. + """ + text = ('"""A string"""\n\n' + '# A comment\n\n' + '# %% A cell\n\n' + 'class Foo(object):\n' + ' def __init__(self):\n' + ' bar = 42\n' + ' print(bar)\n' + ) + show_blanks = CONF.get('editor', 'blank_spaces') + if scheme_name is None: + scheme_name = self.current_scheme + self.preview_editor.setup_editor(linenumbers=True, + markers=True, + tab_mode=False, + font=get_font(), + show_blanks=show_blanks, + color_scheme=scheme_name) + self.preview_editor.set_text(text) + self.preview_editor.set_language('Python') + + # Actions + # ------------------------------------------------------------------------- + def create_new_scheme(self): + """Creates a new color scheme with a custom name.""" + names = self.get_option('names') + custom_names = self.get_option('custom_names', []) + + # Get the available number this new color scheme + counter = len(custom_names) - 1 + custom_index = [int(n.split('-')[-1]) for n in custom_names] + for i in range(len(custom_names)): + if custom_index[i] != i: + counter = i - 1 + break + custom_name = "custom-{0}".format(counter+1) + + # Add the config settings, based on the current one. + custom_names.append(custom_name) + self.set_option('custom_names', custom_names) + for key in syntaxhighlighters.COLOR_SCHEME_KEYS: + name = "{0}/{1}".format(custom_name, key) + default_name = "{0}/{1}".format(self.current_scheme, key) + option = self.get_option(default_name) + self.set_option(name, option) + self.set_option('{0}/name'.format(custom_name), custom_name) + + # Now they need to be loaded! how to make a partial load_from_conf? + dlg = self.scheme_editor_dialog + dlg.add_color_scheme_stack(custom_name, custom=True) + dlg.set_scheme(custom_name) + self.load_from_conf() + + if dlg.exec_(): + # This is needed to have the custom name updated on the combobox + name = dlg.get_scheme_name() + self.set_option('{0}/name'.format(custom_name), name) + + # The +1 is needed because of the separator in the combobox + index = (names + custom_names).index(custom_name) + 1 + self.update_combobox() + self.schemes_combobox.setCurrentIndex(index) + else: + # Delete the config .... + custom_names.remove(custom_name) + self.set_option('custom_names', custom_names) + dlg.delete_color_scheme_stack(custom_name) + + def edit_scheme(self): + """Edit current scheme.""" + dlg = self.scheme_editor_dialog + dlg.set_scheme(self.current_scheme) + + if dlg.exec_(): + # Update temp scheme to reflect instant edits on the preview + temporal_color_scheme = dlg.get_edited_color_scheme() + for key in temporal_color_scheme: + option = "temp/{0}".format(key) + value = temporal_color_scheme[key] + self.set_option(option, value) + self.update_preview(scheme_name='temp') + + def delete_scheme(self): + """Deletes the currently selected custom color scheme.""" + scheme_name = self.current_scheme + + answer = QMessageBox.warning(self, _("Warning"), + _("Are you sure you want to delete " + "this scheme?"), + QMessageBox.Yes | QMessageBox.No) + if answer == QMessageBox.Yes: + # Put the combobox in Spyder by default, when deleting a scheme + names = self.get_option('names') + self.set_scheme('spyder') + self.schemes_combobox.setCurrentIndex(names.index('spyder')) + self.set_option('selected', 'spyder') + + # Delete from custom_names + custom_names = self.get_option('custom_names', []) + if scheme_name in custom_names: + custom_names.remove(scheme_name) + self.set_option('custom_names', custom_names) + + # Delete config options + for key in syntaxhighlighters.COLOR_SCHEME_KEYS: + option = "{0}/{1}".format(scheme_name, key) + CONF.remove_option(self.CONF_SECTION, option) + CONF.remove_option(self.CONF_SECTION, "{0}/name".format(scheme_name)) + + self.update_combobox() + self.update_preview() + + def set_scheme(self, scheme_name): + """ + Set the current stack in the dialog to the scheme with 'scheme_name'. + """ + dlg = self.scheme_editor_dialog + dlg.set_scheme(scheme_name) + + @Slot() + def reset_to_default(self): + """Restore initial values for default color schemes.""" + # Checks that this is indeed a default scheme + scheme = self.current_scheme + names = self.get_option('names') + if scheme in names: + for key in syntaxhighlighters.COLOR_SCHEME_KEYS: + option = "{0}/{1}".format(scheme, key) + value = CONF.get_default(self.CONF_SECTION, option) + self.set_option(option, value) + + self.load_from_conf() + + +class SchemeEditor(QDialog): + """A color scheme editor dialog.""" + def __init__(self, parent=None, stack=None): + super(SchemeEditor, self).__init__(parent) + self.parent = parent + self.stack = stack + self.order = [] # Uses scheme names + + # Needed for self.get_edited_color_scheme() + self.widgets = {} + self.scheme_name_textbox = {} + self.last_edited_color_scheme = None + self.last_used_scheme = None + + # Widgets + bbox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) + + # Layout + layout = QVBoxLayout() + layout.addWidget(self.stack) + layout.addWidget(bbox) + self.setLayout(layout) + + # Signals + bbox.accepted.connect(self.accept) + bbox.accepted.connect(self.get_edited_color_scheme) + bbox.rejected.connect(self.reject) + + # Helpers + # ------------------------------------------------------------------------- + def set_scheme(self, scheme_name): + """Set the current stack by 'scheme_name'.""" + self.stack.setCurrentIndex(self.order.index(scheme_name)) + self.last_used_scheme = scheme_name + + def get_scheme_name(self): + """ + Returns the edited scheme name, needed to update the combobox on + scheme creation. + """ + return self.scheme_name_textbox[self.last_used_scheme].text() + + def get_edited_color_scheme(self): + """ + Get the values of the last edited color scheme to be used in an instant + preview in the preview editor, without using `apply`. + """ + color_scheme = {} + scheme_name = self.last_used_scheme + + for key in self.widgets[scheme_name]: + items = self.widgets[scheme_name][key] + + if len(items) == 1: + # ColorLayout + value = items[0].text() + else: + # ColorLayout + checkboxes + value = (items[0].text(), items[1].isChecked(), + items[2].isChecked()) + + color_scheme[key] = value + + return color_scheme + + # Actions + # ------------------------------------------------------------------------- + def add_color_scheme_stack(self, scheme_name, custom=False): + """Add a stack for a given scheme and connects the CONF values.""" + color_scheme_groups = [ + (_('Text'), ["normal", "comment", "string", "number", "keyword", + "builtin", "definition", "instance", ]), + (_('Highlight'), ["currentcell", "currentline", "occurrence", + "matched_p", "unmatched_p", "ctrlclick"]), + (_('Background'), ["background", "sideareas"]) + ] + + parent = self.parent + line_edit = parent.create_lineedit(_("Scheme name:"), + '{0}/name'.format(scheme_name)) + + self.widgets[scheme_name] = {} + + # Widget setup + line_edit.label.setAlignment(Qt.AlignRight | Qt.AlignVCenter) + self.setWindowTitle(_('Color scheme editor')) + + # Layout + name_layout = QHBoxLayout() + name_layout.addWidget(line_edit.label) + name_layout.addWidget(line_edit.textbox) + self.scheme_name_textbox[scheme_name] = line_edit.textbox + + if not custom: + line_edit.textbox.setDisabled(True) + + cs_layout = QVBoxLayout() + cs_layout.addLayout(name_layout) + + h_layout = QHBoxLayout() + v_layout = QVBoxLayout() + + for index, item in enumerate(color_scheme_groups): + group_name, keys = item + group_layout = QGridLayout() + + for row, key in enumerate(keys): + option = "{0}/{1}".format(scheme_name, key) + value = self.parent.get_option(option) + name = syntaxhighlighters.COLOR_SCHEME_KEYS[key] + + if is_text_string(value): + label, clayout = parent.create_coloredit( + name, + option, + without_layout=True, + ) + label.setAlignment(Qt.AlignRight | Qt.AlignVCenter) + group_layout.addWidget(label, row+1, 0) + group_layout.addLayout(clayout, row+1, 1) + + # Needed to update temp scheme to obtain instant preview + self.widgets[scheme_name][key] = [clayout] + else: + label, clayout, cb_bold, cb_italic = parent.create_scedit( + name, + option, + without_layout=True, + ) + label.setAlignment(Qt.AlignRight | Qt.AlignVCenter) + group_layout.addWidget(label, row+1, 0) + group_layout.addLayout(clayout, row+1, 1) + group_layout.addWidget(cb_bold, row+1, 2) + group_layout.addWidget(cb_italic, row+1, 3) + + # Needed to update temp scheme to obtain instant preview + self.widgets[scheme_name][key] = [clayout, cb_bold, + cb_italic] + + group_box = QGroupBox(group_name) + group_box.setLayout(group_layout) + + if index == 0: + h_layout.addWidget(group_box) + else: + v_layout.addWidget(group_box) + + h_layout.addLayout(v_layout) + cs_layout.addLayout(h_layout) + + stackitem = QWidget() + stackitem.setLayout(cs_layout) + self.stack.addWidget(stackitem) + self.order.append(scheme_name) + + def delete_color_scheme_stack(self, scheme_name): + """Remove stack widget by 'scheme_name'.""" + self.set_scheme(scheme_name) + widget = self.stack.currentWidget() + self.stack.removeWidget(widget) + index = self.order.index(scheme_name) + self.order.pop(index) diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/plugins/console.py spyder-3.0.2+dfsg1/spyder/plugins/console.py --- spyder-2.3.8+dfsg1/spyder/plugins/console.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/plugins/console.py 2016-11-19 01:31:58.000000000 +0100 @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- # -# Copyright © 2009-2010 Pierre Raybaut +# Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) +# (see spyder/__init__.py for details) """Internal Console Plugin""" @@ -11,38 +11,47 @@ # pylint: disable=R0911 # pylint: disable=R0201 -from spyderlib.qt.QtGui import (QVBoxLayout, QFontDialog, QInputDialog, - QLineEdit, QMenu) -from spyderlib.qt.QtCore import SIGNAL -from spyderlib.qt.compat import getopenfilename - +# Standard library imports import os -import sys import os.path as osp +import sys +# Third party imports +from qtpy import PYQT5 +from qtpy.compat import getopenfilename +from qtpy.QtCore import Signal, Slot +from qtpy.QtWidgets import QInputDialog, QLineEdit, QMenu, QVBoxLayout # Local imports -from spyderlib.baseconfig import _, debug_print -from spyderlib.config import CONF -from spyderlib.utils.misc import get_error_match, remove_backslashes -from spyderlib.utils.qthelpers import (get_icon, create_action, add_actions, - mimedata2url, DialogManager) -from spyderlib.utils.environ import EnvDialog -from spyderlib.widgets.internalshell import InternalShell -from spyderlib.widgets.findreplace import FindReplace -from spyderlib.widgets.dicteditor import DictEditor -from spyderlib.plugins import SpyderPluginWidget -from spyderlib.py3compat import to_text_string, getcwd +from spyder.config.base import _, debug_print +from spyder.config.main import CONF +from spyder.utils import icon_manager as ima +from spyder.utils.environ import EnvDialog +from spyder.utils.misc import get_error_match, remove_backslashes +from spyder.utils.qthelpers import (add_actions, create_action, + DialogManager, mimedata2url) +from spyder.widgets.internalshell import InternalShell +from spyder.widgets.findreplace import FindReplace +from spyder.widgets.variableexplorer.collectionseditor import CollectionsEditor +from spyder.plugins import SpyderPluginWidget +from spyder.py3compat import getcwd, to_text_string + - class Console(SpyderPluginWidget): """ Console widget """ CONF_SECTION = 'internal_console' + focus_changed = Signal() + redirect_stdio = Signal(bool) + edit_goto = Signal(str, int, str) + def __init__(self, parent=None, namespace=None, commands=[], message=None, exitfunc=None, profile=False, multithreaded=False): - SpyderPluginWidget.__init__(self, parent) + if PYQT5: + SpyderPluginWidget.__init__(self, parent, main = parent) + else: + SpyderPluginWidget.__init__(self, parent) debug_print(" ..internal console: initializing") self.dialog_manager = DialogManager() @@ -54,26 +63,22 @@ self.get_plugin_font(), exitfunc, profile, multithreaded, light_background=light_background) - self.connect(self.shell, SIGNAL('status(QString)'), - lambda msg: - self.emit(SIGNAL('show_message(QString,int)'), msg, 0)) - self.connect(self.shell, SIGNAL("go_to_error(QString)"), - self.go_to_error) - self.connect(self.shell, SIGNAL("focus_changed()"), - lambda: self.emit(SIGNAL("focus_changed()"))) - # Redirecting some SIGNALs: - self.connect(self.shell, SIGNAL('redirect_stdio(bool)'), - lambda state: self.emit(SIGNAL('redirect_stdio(bool)'), - state)) + self.shell.status.connect(lambda msg: self.show_message.emit(msg, 0)) + self.shell.go_to_error.connect(self.go_to_error) + self.shell.focus_changed.connect(lambda: self.focus_changed.emit()) + + # Redirecting some signals: + self.shell.redirect_stdio.connect(lambda state: + self.redirect_stdio.emit(state)) # Initialize plugin self.initialize_plugin() - + # Find/replace widget self.find_widget = FindReplace(self) self.find_widget.set_editor(self.shell) self.find_widget.hide() - self.register_widget_shortcuts("Editor", self.find_widget) + self.register_widget_shortcuts(self.find_widget) # Main layout layout = QVBoxLayout() @@ -92,12 +97,11 @@ """Bind historylog instance to this console Not used anymore since v2.0""" historylog.add_history(self.shell.history_filename) - self.connect(self.shell, SIGNAL('append_to_history(QString,QString)'), - historylog.append_to_history) - - def set_inspector(self, inspector): - """Bind inspector instance to this console""" - self.shell.inspector = inspector + self.shell.append_to_history.connect(historylog.append_to_history) + + def set_help(self, help_plugin): + """Bind help instance to this console""" + self.shell.help = help_plugin #------ SpyderPluginWidget API --------------------------------------------- def get_plugin_title(self): @@ -110,7 +114,12 @@ this plugin's dockwidget is raised on top-level """ return self.shell - + + def update_font(self): + """Update font from Preferences""" + font = self.get_plugin_font() + self.shell.set_font(font) + def closing_plugin(self, cancelable=False): """Perform actions before parent main window is closed""" self.dialog_manager.close_all() @@ -123,31 +132,29 @@ def get_plugin_actions(self): """Return a list of actions related to plugin""" quit_action = create_action(self, _("&Quit"), - icon='exit.png', tip=_("Quit"), + icon=ima.icon('exit'), + tip=_("Quit"), triggered=self.quit) self.register_shortcut(quit_action, "_", "Quit", "Ctrl+Q") run_action = create_action(self, _("&Run..."), None, - 'run_small.png', _("Run a Python script"), + ima.icon('run_small'), + _("Run a Python script"), triggered=self.run_script) environ_action = create_action(self, _("Environment variables..."), - icon = 'environ.png', + icon=ima.icon('environ'), tip=_("Show and edit environment variables" " (for current session)"), triggered=self.show_env) syspath_action = create_action(self, _("Show sys.path contents..."), - icon = 'syspath.png', + icon=ima.icon('syspath'), tip=_("Show (read-only) sys.path"), triggered=self.show_syspath) buffer_action = create_action(self, _("Buffer..."), None, tip=_("Set maximum line count"), triggered=self.change_max_line_count) - font_action = create_action(self, - _("&Font..."), None, - 'font.png', _("Set shell font style"), - triggered=self.change_font) exteditor_action = create_action(self, _("External editor path..."), None, None, _("Set external editor executable path"), @@ -169,9 +176,9 @@ codecompenter_action.setChecked(self.get_option( 'codecompletion/enter_key')) - option_menu = QMenu(_("Internal console settings"), self) - option_menu.setIcon(get_icon('tooloptions.png')) - add_actions(option_menu, (buffer_action, font_action, wrap_action, + option_menu = QMenu(_('Internal console settings'), self) + option_menu.setIcon(ima.icon('tooloptions')) + add_actions(option_menu, (buffer_action, wrap_action, calltips_action, codecompletion_action, codecompenter_action, exteditor_action)) @@ -185,12 +192,10 @@ def register_plugin(self): """Register plugin in Spyder's main window""" - self.connect(self, SIGNAL('focus_changed()'), - self.main.plugin_focus_changed) + self.focus_changed.connect(self.main.plugin_focus_changed) self.main.add_dockwidget(self) # Connecting the following signal once the dockwidget has been created: - self.connect(self.shell, SIGNAL('traceback_available()'), - self.traceback_available) + self.shell.traceback_available.connect(self.traceback_available) def traceback_available(self): """Traceback is available in the internal console: showing the @@ -200,21 +205,25 @@ self.dockwidget.raise_() #------ Public API --------------------------------------------------------- + @Slot() def quit(self): """Quit mainwindow""" self.main.close() - + + @Slot() def show_env(self): """Show environment variables""" self.dialog_manager.show(EnvDialog()) - + + @Slot() def show_syspath(self): """Show sys.path""" - editor = DictEditor() + editor = CollectionsEditor() editor.setup(sys.path, title="sys.path", readonly=True, - width=600, icon='syspath.png') + width=600, icon=ima.icon('syspath')) self.dialog_manager.show(editor) - + + @Slot() def run_script(self, filename=None, silent=False, set_focus=False, args=None): """Run a Python script""" @@ -228,6 +237,7 @@ filename = osp.basename(filename) else: return + debug_print(args) filename = osp.abspath(filename) rbs = remove_backslashes command = "runfile('%s', args='%s')" % (rbs(filename), rbs(args)) @@ -255,25 +265,17 @@ self.shell.external_editor(filename, goto) return if filename is not None: - self.emit(SIGNAL("edit_goto(QString,int,QString)"), - osp.abspath(filename), goto, '') + self.edit_goto.emit(osp.abspath(filename), goto, '') def execute_lines(self, lines): """Execute lines and give focus to shell""" self.shell.execute_lines(to_text_string(lines)) self.shell.setFocus() - - def change_font(self): - """Change console font""" - font, valid = QFontDialog.getFont(self.get_plugin_font(), - self, _("Select a new font")) - if valid: - self.shell.set_font(font) - self.set_plugin_font(font) - + + @Slot() def change_max_line_count(self): "Change maximum line count""" - mlc, valid = QInputDialog.getInteger(self, _('Buffer'), + mlc, valid = QInputDialog.getInt(self, _('Buffer'), _('Maximum line count'), self.get_option('max_line_count'), 0, 1000000) @@ -281,6 +283,7 @@ self.shell.setMaximumBlockCount(mlc) self.set_option('max_line_count', mlc) + @Slot() def change_exteditor(self): """Change external editor path""" path, valid = QInputDialog.getText(self, _('External editor'), @@ -289,22 +292,26 @@ self.get_option('external_editor/path')) if valid: self.set_option('external_editor/path', to_text_string(path)) - + + @Slot(bool) def toggle_wrap_mode(self, checked): """Toggle wrap mode""" self.shell.toggle_wrap_mode(checked) self.set_option('wrap', checked) - + + @Slot(bool) def toggle_calltips(self, checked): """Toggle calltips""" self.shell.set_calltips(checked) self.set_option('calltips', checked) - + + @Slot(bool) def toggle_codecompletion(self, checked): """Toggle automatic code completion""" self.shell.set_codecompletion_auto(checked) self.set_option('codecompletion/auto', checked) - + + @Slot(bool) def toggle_codecompletion_enter(self, checked): """Toggle Enter key for code completion""" self.shell.set_codecompletion_enter(checked) diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/plugins/editor.py spyder-3.0.2+dfsg1/spyder/plugins/editor.py --- spyder-2.3.8+dfsg1/spyder/plugins/editor.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/plugins/editor.py 2016-11-19 01:31:58.000000000 +0100 @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- # -# Copyright © 2009-2010 Pierre Raybaut +# Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) +# (see spyder/__init__.py for details) """Editor Plugin""" @@ -11,42 +11,46 @@ # pylint: disable=R0911 # pylint: disable=R0201 -from spyderlib.qt.QtGui import (QVBoxLayout, QPrintDialog, QSplitter, QToolBar, - QAction, QApplication, QDialog, QWidget, - QPrinter, QActionGroup, QInputDialog, QMenu, - QAbstractPrintDialog, QGroupBox, QTabWidget, - QLabel, QFontComboBox, QHBoxLayout, - QKeySequence) -from spyderlib.qt.QtCore import SIGNAL, QByteArray, Qt, Slot -from spyderlib.qt.compat import to_qvariant, from_qvariant, getopenfilenames - +# Standard library imports import os +import os.path as osp import re -import sys import time -import os.path as osp -# Local imports -from spyderlib.utils import encoding, sourcecode, codeanalysis -from spyderlib.baseconfig import get_conf_path, _ -from spyderlib.config import CONF, EDIT_FILTERS, get_filter, EDIT_FILETYPES -from spyderlib.guiconfig import get_color_scheme -from spyderlib.utils import programs -from spyderlib.utils.qthelpers import (get_icon, create_action, add_actions, - get_std_icon, get_filetype_icon, - add_shortcut_to_tooltip) -from spyderlib.widgets.findreplace import FindReplace -from spyderlib.widgets.status import (ReadWriteStatus, EOLStatus, - EncodingStatus, CursorPositionStatus) -from spyderlib.widgets.editor import (EditorSplitter, EditorStack, Printer, - EditorMainWindow) -from spyderlib.widgets.sourcecode.codeeditor import CodeEditor -from spyderlib.plugins import SpyderPluginWidget, PluginConfigPage -from spyderlib.plugins.runconfig import (RunConfigDialog, RunConfigOneDialog, - get_run_configuration, - ALWAYS_OPEN_FIRST_RUN_OPTION) -from spyderlib.py3compat import PY2, to_text_string, getcwd, qbytearray_to_str +# Third party imports +from qtpy import API, PYQT5 +from qtpy.compat import from_qvariant, getopenfilenames, to_qvariant +from qtpy.QtCore import QByteArray, Qt, Signal, Slot +from qtpy.QtGui import QKeySequence +from qtpy.QtPrintSupport import QAbstractPrintDialog, QPrintDialog, QPrinter +from qtpy.QtWidgets import (QAction, QActionGroup, QApplication, QDialog, + QFileDialog, QGridLayout, QGroupBox, QHBoxLayout, + QInputDialog, QLabel, QMenu, QSplitter, QTabWidget, + QToolBar, QVBoxLayout, QWidget) +# Local imports +from spyder.config.base import _, get_conf_path +from spyder.config.gui import (RUN_CELL_SHORTCUT, + RUN_CELL_AND_ADVANCE_SHORTCUT) +from spyder.config.main import CONF +from spyder.config.utils import (get_edit_filetypes, get_edit_filters, + get_filter) +from spyder.py3compat import getcwd, PY2, qbytearray_to_str, to_text_string +from spyder.utils import codeanalysis, encoding, programs, sourcecode +from spyder.utils import icon_manager as ima +from spyder.utils.introspection.manager import IntrospectionManager +from spyder.utils.qthelpers import add_actions, create_action +from spyder.widgets.findreplace import FindReplace +from spyder.widgets.editor import (EditorMainWindow, EditorSplitter, + EditorStack, Printer) +from spyder.widgets.sourcecode.codeeditor import CodeEditor +from spyder.widgets.status import (CursorPositionStatus, EncodingStatus, + EOLStatus, ReadWriteStatus) +from spyder.plugins import SpyderPluginWidget +from spyder.plugins.configdialog import PluginConfigPage +from spyder.plugins.runconfig import (ALWAYS_OPEN_FIRST_RUN_OPTION, + get_run_configuration, + RunConfigDialog, RunConfigOneDialog) def _load_all_breakpoints(): @@ -94,16 +98,13 @@ return _("Editor") def get_icon(self): - return get_icon("edit24.png") + return ima.icon('edit') def setup_page(self): template_btn = self.create_button(_("Edit template for new modules"), self.plugin.edit_template) interface_group = QGroupBox(_("Interface")) - font_group = self.create_fontgroup(option=None, - text=_("Text and margin font style"), - fontfilters=QFontComboBox.MonospacedFonts) newcb = self.create_checkbox fpsorting_box = newcb(_("Sort files according to full path"), 'fullpath_sorting') @@ -120,43 +121,39 @@ edgeline_box = newcb(_("Show vertical line after"), 'edge_line') edgeline_spin = self.create_spinbox("", _("characters"), 'edge_line_column', 79, 1, 500) - self.connect(edgeline_box, SIGNAL("toggled(bool)"), - edgeline_spin.setEnabled) + edgeline_box.toggled.connect(edgeline_spin.setEnabled) edgeline_spin.setEnabled(self.get_option('edge_line')) - edgeline_layout = QHBoxLayout() - edgeline_layout.addWidget(edgeline_box) - edgeline_layout.addWidget(edgeline_spin) + currentline_box = newcb(_("Highlight current line"), 'highlight_current_line') currentcell_box = newcb(_("Highlight current cell"), 'highlight_current_cell') - occurence_box = newcb(_("Highlight occurences after"), - 'occurence_highlighting') - occurence_spin = self.create_spinbox("", " ms", - 'occurence_highlighting/timeout', + occurrence_box = newcb(_("Highlight occurrences after"), + 'occurrence_highlighting') + occurrence_spin = self.create_spinbox("", _(" ms"), + 'occurrence_highlighting/timeout', min_=100, max_=1000000, step=100) - self.connect(occurence_box, SIGNAL("toggled(bool)"), - occurence_spin.setEnabled) - occurence_spin.setEnabled(self.get_option('occurence_highlighting')) - occurence_layout = QHBoxLayout() - occurence_layout.addWidget(occurence_box) - occurence_layout.addWidget(occurence_spin) + occurrence_box.toggled.connect(occurrence_spin.setEnabled) + occurrence_spin.setEnabled(self.get_option('occurrence_highlighting')) + wrap_mode_box = newcb(_("Wrap lines"), 'wrap') - names = CONF.get('color_schemes', 'names') - choices = list(zip(names, names)) - cs_combo = self.create_combobox(_("Syntax color scheme: "), - choices, 'color_scheme_name') - - display_layout = QVBoxLayout() - display_layout.addWidget(linenumbers_box) - display_layout.addWidget(blanks_box) - display_layout.addLayout(edgeline_layout) - display_layout.addWidget(currentline_box) - display_layout.addWidget(currentcell_box) - display_layout.addLayout(occurence_layout) - display_layout.addWidget(wrap_mode_box) - display_layout.addWidget(cs_combo) - display_group.setLayout(display_layout) + + display_layout = QGridLayout() + display_layout.addWidget(linenumbers_box, 0, 0) + display_layout.addWidget(blanks_box, 1, 0) + display_layout.addWidget(edgeline_box, 2, 0) + display_layout.addWidget(edgeline_spin.spinbox, 2, 1) + display_layout.addWidget(edgeline_spin.slabel, 2, 2) + display_layout.addWidget(currentline_box, 3, 0) + display_layout.addWidget(currentcell_box, 4, 0) + display_layout.addWidget(occurrence_box, 5, 0) + display_layout.addWidget(occurrence_spin.spinbox, 5, 1) + display_layout.addWidget(occurrence_spin.slabel, 5, 2) + display_layout.addWidget(wrap_mode_box, 6, 0) + display_h_layout = QHBoxLayout() + display_h_layout.addLayout(display_layout) + display_h_layout.addStretch(1) + display_group.setLayout(display_h_layout) run_group = QGroupBox(_("Run")) saveall_box = newcb(_("Save all files before running script"), @@ -201,9 +198,14 @@ autounindent_box = newcb(_("Automatic indentation after 'else', " "'elif', etc."), 'auto_unindent') indent_chars_box = self.create_combobox(_("Indentation characters: "), - ((_("4 spaces"), '* *'), - (_("2 spaces"), '* *'), - (_("tab"), '*\t*')), 'indent_chars') + ((_("2 spaces"), '* *'), + (_("3 spaces"), '* *'), + (_("4 spaces"), '* *'), + (_("5 spaces"), '* *'), + (_("6 spaces"), '* *'), + (_("7 spaces"), '* *'), + (_("8 spaces"), '* *'), + (_("Tabulations"), '*\t*')), 'indent_chars') tabwidth_spin = self.create_spinbox(_("Tab stop width:"), _("pixels"), 'tab_stop_width', 40, 10, 1000, 10) tab_mode_box = newcb(_("Tab always indent"), @@ -220,35 +222,33 @@ 'always_remove_trailing_spaces', default=False) analysis_group = QGroupBox(_("Analysis")) - pep8_url = 'PEP8' - analysis_label = QLabel(_("Note: add analysis:ignore in " - "a comment to ignore code/style analysis " - "warnings. For more informations on style " - "guide for Python code, please refer to the " - "%s page.") % pep8_url) - analysis_label.setWordWrap(True) + pep_url = 'PEP8' + pep8_label = QLabel(_("(Refer to the {} page)").format(pep_url)) + pep8_label.setOpenExternalLinks(True) is_pyflakes = codeanalysis.is_pyflakes_installed() is_pep8 = codeanalysis.get_checker_executable('pep8') is not None - analysis_label.setEnabled(is_pyflakes or is_pep8) - pyflakes_box = newcb(_("Code analysis")+" (pyflakes)", + pyflakes_box = newcb(_("Real-time code analysis"), 'code_analysis/pyflakes', default=True, - tip=_("If enabled, Python source code will be analyzed\n" - "using pyflakes, lines containing errors or \n" - "warnings will be highlighted")) + tip=_("

    If enabled, Python source code will be analyzed " + "using pyflakes, lines containing errors or " + "warnings will be highlighted.

    " + "

    Note: add analysis:ignore in " + "a comment to ignore code analysis " + "warnings.

    ")) pyflakes_box.setEnabled(is_pyflakes) if not is_pyflakes: pyflakes_box.setToolTip(_("Code analysis requires pyflakes %s+") % codeanalysis.PYFLAKES_REQVER) - pep8_box = newcb(_("Style analysis")+' (pep8)', + pep8_box = newcb(_("Real-time code style analysis"), 'code_analysis/pep8', default=False, - tip=_('If enabled, Python source code will be analyzed\n' - 'using pep8, lines that are not following PEP8\n' - 'style guide will be highlighted')) + tip=_("

    If enabled, Python source code will be analyzed" + "using pep8, lines that are not following PEP8 " + "style guide will be highlighted.

    " + "

    Note: add analysis:ignore in " + "a comment to ignore style analysis " + "warnings.

    ")) pep8_box.setEnabled(is_pep8) - ancb_layout = QHBoxLayout() - ancb_layout.addWidget(pyflakes_box) - ancb_layout.addWidget(pep8_box) - todolist_box = newcb(_("Tasks (TODO, FIXME, XXX, HINT, TIP, @todo)"), + todolist_box = newcb(_("Code annotations (TODO, FIXME, XXX, HINT, TIP, @todo)"), 'todo_list', default=True) realtime_radio = self.create_radiobutton( _("Perform analysis when " @@ -258,7 +258,7 @@ _("Perform analysis only " "when saving file"), 'onsave_analysis') - af_spin = self.create_spinbox("", " ms", 'realtime_analysis/timeout', + af_spin = self.create_spinbox("", _(" ms"), 'realtime_analysis/timeout', min_=100, max_=1000000, step=100) af_layout = QHBoxLayout() af_layout.addWidget(realtime_radio) @@ -284,8 +284,11 @@ introspection_group.setLayout(introspection_layout) analysis_layout = QVBoxLayout() - analysis_layout.addWidget(analysis_label) - analysis_layout.addLayout(ancb_layout) + analysis_layout.addWidget(pyflakes_box) + analysis_pep_layout = QHBoxLayout() + analysis_pep_layout.addWidget(pep8_box) + analysis_pep_layout.addWidget(pep8_label) + analysis_layout.addLayout(analysis_pep_layout) analysis_layout.addWidget(todolist_box) analysis_layout.addLayout(af_layout) analysis_layout.addWidget(saveonly_radio) @@ -296,8 +299,16 @@ sourcecode_layout.addWidget(autounindent_box) sourcecode_layout.addWidget(add_colons_box) sourcecode_layout.addWidget(close_quotes_box) - sourcecode_layout.addWidget(indent_chars_box) - sourcecode_layout.addWidget(tabwidth_spin) + indent_tab_layout = QHBoxLayout() + indent_tab_grid_layout = QGridLayout() + indent_tab_grid_layout.addWidget(indent_chars_box.label, 0, 0) + indent_tab_grid_layout.addWidget(indent_chars_box.combobox, 0, 1) + indent_tab_grid_layout.addWidget(tabwidth_spin.plabel, 1, 0) + indent_tab_grid_layout.addWidget(tabwidth_spin.spinbox, 1, 1) + indent_tab_grid_layout.addWidget(tabwidth_spin.slabel, 1, 2) + indent_tab_layout.addLayout(indent_tab_grid_layout) + indent_tab_layout.addStretch(1) + sourcecode_layout.addLayout(indent_tab_layout) sourcecode_layout.addWidget(tab_mode_box) sourcecode_layout.addWidget(ibackspace_box) sourcecode_layout.addWidget(removetrail_box) @@ -320,7 +331,7 @@ eol_group.setLayout(eol_layout) tabs = QTabWidget() - tabs.addTab(self.create_tab(font_group, interface_group, display_group), + tabs.addTab(self.create_tab(interface_group, display_group), _("Display")) tabs.addTab(self.create_tab(introspection_group, analysis_group), _("Code Introspection/Analysis")) @@ -342,27 +353,43 @@ TEMPFILE_PATH = get_conf_path('temp.py') TEMPLATE_PATH = get_conf_path('template.py') DISABLE_ACTIONS_WHEN_HIDDEN = False # SpyderPluginWidget class attribute + + # Signals + run_in_current_ipyclient = Signal(str, str, str, bool, bool) + exec_in_extconsole = Signal(str, bool) + redirect_stdio = Signal(bool) + open_dir = Signal(str) + breakpoints_saved = Signal() + run_in_current_extconsole = Signal(str, str, str, bool, bool) + def __init__(self, parent, ignore_last_opened_files=False): - SpyderPluginWidget.__init__(self, parent) - + if PYQT5: + SpyderPluginWidget.__init__(self, parent, main=parent) + else: + SpyderPluginWidget.__init__(self, parent) + self.__set_eol_chars = True - - self.set_default_color_scheme() - + # Creating template if it doesn't already exist if not osp.isfile(self.TEMPLATE_PATH): - header = ['# -*- coding: utf-8 -*-', '"""', 'Created on %(date)s', - '', '@author: %(username)s', '"""', ''] + if os.name == "nt": + shebang = [] + else: + shebang = ['#!/usr/bin/env python' + ('2' if PY2 else '3')] + header = shebang + [ + '# -*- coding: utf-8 -*-', + '"""', 'Created on %(date)s', '', + '@author: %(username)s', '"""', ''] encoding.write(os.linesep.join(header), self.TEMPLATE_PATH, 'utf-8') - self.projectexplorer = None + self.projects = None self.outlineexplorer = None - self.inspector = None + self.help = None self.editorstacks = None self.editorwindows = None self.editorwindows_to_be_created = None - + self.file_dependent_actions = [] self.pythonfile_dependent_actions = [] self.dock_toolbar_actions = None @@ -399,21 +426,21 @@ self.editorwindows_to_be_created = [] self.toolbar_list = None self.menu_list = None - + + self.introspector = IntrospectionManager() + # Setup new windows: - self.connect(self.main, SIGNAL('all_actions_defined()'), - self.setup_other_windows) + self.main.all_actions_defined.connect(self.setup_other_windows) # Change module completions when PYTHONPATH changes - self.connect(self.main, SIGNAL("pythonpath_changed()"), - self.set_path) + self.main.sig_pythonpath_changed.connect(self.set_path) # Find widget self.find_widget = FindReplace(self, enable_replace=True) self.find_widget.hide() - self.connect(self.find_widget, SIGNAL("visibility_changed(bool)"), - lambda vs: self.rehighlight_cells()) - self.register_widget_shortcuts("Editor", self.find_widget) + self.find_widget.visibility_changed.connect( + lambda vs: self.rehighlight_cells()) + self.register_widget_shortcuts(self.find_widget) # Tabbed editor widget + Find/Replace widget editor_widgets = QWidget(self) @@ -437,30 +464,20 @@ # Editor's splitter state state = self.get_option('splitter_state', None) if state is not None: - self.splitter.restoreState( QByteArray().fromHex(str(state)) ) + self.splitter.restoreState( QByteArray().fromHex( + str(state).encode('utf-8')) ) self.recent_files = self.get_option('recent_files', []) - self.untitled_num = 0 - - filenames = self.get_option('filenames', []) - if filenames and not ignore_last_opened_files: - self.load(filenames) - layout = self.get_option('layout_settings', None) - if layout is not None: - self.editorsplitter.set_layout_settings(layout) - win_layout = self.get_option('windows_layout_settings', None) - if win_layout: - for layout_settings in win_layout: - self.editorwindows_to_be_created.append(layout_settings) - self.set_last_focus_editorstack(self, self.editorstacks[0]) - else: - self.__load_temp_file() - + # Parameters of last file execution: self.__last_ic_exec = None # internal console self.__last_ec_exec = None # external console - + + # File types and filters used by the Open dialog + self.edit_filetypes = None + self.edit_filters = None + self.__ignore_cursor_position = False current_editor = self.get_current_editor() if current_editor is not None: @@ -470,12 +487,13 @@ self.update_cursorpos_actions() self.set_path() - def set_projectexplorer(self, projectexplorer): - self.projectexplorer = projectexplorer - - def show_hide_project_explorer(self): - if self.projectexplorer is not None: - dw = self.projectexplorer.dockwidget + def set_projects(self, projects): + self.projects = projects + + @Slot() + def show_hide_projects(self): + if self.projects is not None: + dw = self.projects.dockwidget if dw.isVisible(): dw.hide() else: @@ -488,15 +506,15 @@ for editorstack in self.editorstacks: editorstack.set_outlineexplorer(self.outlineexplorer) self.editorstacks[0].initialize_outlineexplorer() - self.connect(self.outlineexplorer, - SIGNAL("edit_goto(QString,int,QString)"), - lambda filenames, goto, word: - self.load(filenames=filenames, goto=goto, word=word, - editorwindow=self)) - self.connect(self.outlineexplorer, SIGNAL("edit(QString)"), - lambda filenames: - self.load(filenames=filenames, editorwindow=self)) - + self.outlineexplorer.edit_goto.connect( + lambda filenames, goto, word: + self.load(filenames=filenames, goto=goto, word=word, + editorwindow=self)) + self.outlineexplorer.edit.connect( + lambda filenames: + self.load(filenames=filenames, editorwindow=self)) + + @Slot() def show_hide_outline_explorer(self): if self.outlineexplorer is not None: dw = self.outlineexplorer.dockwidget @@ -506,12 +524,12 @@ dw.show() dw.raise_() self.switch_to_plugin() - - def set_inspector(self, inspector): - self.inspector = inspector + + def set_help(self, help_plugin): + self.help = help_plugin for editorstack in self.editorstacks: - editorstack.set_inspector(self.inspector) - + editorstack.set_help(self.help) + #------ Private API -------------------------------------------------------- def restore_scrollbar_position(self): """Restoring scrollbar position after main window is visible""" @@ -532,7 +550,7 @@ def get_plugin_icon(self): """Return widget icon""" - return get_icon('edit.png') + return ima.icon('edit') def get_focus_widget(self): """ @@ -563,18 +581,30 @@ self.set_option('splitter_state', qbytearray_to_str(state)) filenames = [] editorstack = self.editorstacks[0] - filenames += [finfo.filename for finfo in editorstack.data] + + active_project_path = None + if self.projects is not None: + active_project_path = self.projects.get_active_project_path() + if not active_project_path: + self.set_open_filenames() + else: + self.projects.set_project_filenames( + [finfo.filename for finfo in editorstack.data]) + self.set_option('layout_settings', self.editorsplitter.get_layout_settings()) self.set_option('windows_layout_settings', [win.get_layout_settings() for win in self.editorwindows]) - self.set_option('filenames', filenames) +# self.set_option('filenames', filenames) self.set_option('recent_files', self.recent_files) - if not editorstack.save_if_changed(cancelable) and cancelable: - return False - else: - for win in self.editorwindows[:]: - win.close() + try: + if not editorstack.save_if_changed(cancelable) and cancelable: + return False + else: + for win in self.editorwindows[:]: + win.close() + return True + except IndexError: return True def get_plugin_actions(self): @@ -587,7 +617,7 @@ name="Show/hide outline") self.toggle_project_action = create_action(self, _("Show/hide project explorer"), - triggered=self.show_hide_project_explorer, + triggered=self.show_hide_projects, context=Qt.WidgetWithChildrenShortcut) self.register_shortcut(self.toggle_project_action, context="Editor", name="Show/hide project explorer") @@ -595,249 +625,264 @@ # ---- File menu and toolbar ---- self.new_action = create_action(self, _("&New file..."), - icon='filenew.png', tip=_("New file"), - triggered=self.new) + icon=ima.icon('filenew'), tip=_("New file"), + triggered=self.new, + context=Qt.WidgetShortcut) self.register_shortcut(self.new_action, context="Editor", - name="New file") - add_shortcut_to_tooltip(self.new_action, context="Editor", - name="New file") - + name="New file", add_sc_to_tip=True) + self.open_action = create_action(self, _("&Open..."), - icon='fileopen.png', tip=_("Open file"), - triggered=self.load) + icon=ima.icon('fileopen'), tip=_("Open file"), + triggered=self.load, + context=Qt.WidgetShortcut) self.register_shortcut(self.open_action, context="Editor", - name="Open file") - add_shortcut_to_tooltip(self.open_action, context="Editor", - name="Open file") - + name="Open file", add_sc_to_tip=True) + + self.file_switcher_action = create_action(self, _('File switcher...'), + icon=ima.icon('filelist'), + tip=_('Fast switch between files'), + triggered=self.call_file_switcher, + context=Qt.ApplicationShortcut) + self.register_shortcut(self.file_switcher_action, context="_", + name="File switcher", add_sc_to_tip=True) + self.revert_action = create_action(self, _("&Revert"), - icon='revert.png', tip=_("Revert file from disk"), + icon=ima.icon('revert'), tip=_("Revert file from disk"), triggered=self.revert) - + self.save_action = create_action(self, _("&Save"), - icon='filesave.png', tip=_("Save file"), - triggered=self.save) + icon=ima.icon('filesave'), tip=_("Save file"), + triggered=self.save, + context=Qt.WidgetShortcut) self.register_shortcut(self.save_action, context="Editor", - name="Save file") - add_shortcut_to_tooltip(self.save_action, context="Editor", - name="Save file") - + name="Save file", add_sc_to_tip=True) + self.save_all_action = create_action(self, _("Sav&e all"), - icon='save_all.png', tip=_("Save all files"), - triggered=self.save_all) + icon=ima.icon('save_all'), tip=_("Save all files"), + triggered=self.save_all, + context=Qt.WidgetShortcut) self.register_shortcut(self.save_all_action, context="Editor", - name="Save all") - add_shortcut_to_tooltip(self.save_all_action, context="Editor", - name="Save all") - + name="Save all", add_sc_to_tip=True) + save_as_action = create_action(self, _("Save &as..."), None, - 'filesaveas.png', _("Save current file as..."), - triggered=self.save_as) + ima.icon('filesaveas'), tip=_("Save current file as..."), + triggered=self.save_as, + context=Qt.WidgetShortcut) + self.register_shortcut(save_as_action, "Editor", "Save As") + print_preview_action = create_action(self, _("Print preview..."), tip=_("Print preview..."), triggered=self.print_preview) self.print_action = create_action(self, _("&Print..."), - icon='print.png', tip=_("Print current file..."), + icon=ima.icon('print'), tip=_("Print current file..."), triggered=self.print_file) - self.register_shortcut(self.print_action, context="Editor", - name="Print") # Shortcut for close_action is defined in widgets/editor.py self.close_action = create_action(self, _("&Close"), - icon='fileclose.png', tip=_("Close current file"), + icon=ima.icon('fileclose'), tip=_("Close current file"), triggered=self.close_file) + self.close_all_action = create_action(self, _("C&lose all"), - icon='filecloseall.png', tip=_("Close all opened files"), - triggered=self.close_all_files) + icon=ima.icon('filecloseall'), tip=_("Close all opened files"), + triggered=self.close_all_files, + context=Qt.WidgetShortcut) self.register_shortcut(self.close_all_action, context="Editor", name="Close all") - # ---- Debug menu ---- + # ---- Find menu and toolbar ---- + _text = _("&Find text") + find_action = create_action(self, _text, icon=ima.icon('find'), + tip=_text, triggered=self.find, + context=Qt.WidgetShortcut) + self.register_shortcut(find_action, context="_", + name="Find text", add_sc_to_tip=True) + find_next_action = create_action(self, _("Find &next"), + icon=ima.icon('findnext'), + triggered=self.find_next, + context=Qt.WidgetShortcut) + self.register_shortcut(find_next_action, context="_", + name="Find next") + find_previous_action = create_action(self, _("Find &previous"), + icon=ima.icon('findprevious'), + triggered=self.find_previous, + context=Qt.WidgetShortcut) + self.register_shortcut(find_previous_action, context="_", + name="Find previous") + _text = _("&Replace text") + replace_action = create_action(self, _text, icon=ima.icon('replace'), + tip=_text, triggered=self.replace, + context=Qt.WidgetShortcut) + self.register_shortcut(replace_action, context="_", + name="Replace text") + + # ---- Debug menu and toolbar ---- set_clear_breakpoint_action = create_action(self, _("Set/Clear breakpoint"), - icon=get_icon("breakpoint_big.png"), + icon=ima.icon('breakpoint_big'), triggered=self.set_or_clear_breakpoint, context=Qt.WidgetShortcut) self.register_shortcut(set_clear_breakpoint_action, context="Editor", name="Breakpoint") set_cond_breakpoint_action = create_action(self, _("Set/Edit conditional breakpoint"), - icon=get_icon("breakpoint_cond_big.png"), + icon=ima.icon('breakpoint_cond_big'), triggered=self.set_or_edit_conditional_breakpoint, context=Qt.WidgetShortcut) self.register_shortcut(set_cond_breakpoint_action, context="Editor", name="Conditional breakpoint") clear_all_breakpoints_action = create_action(self, - _("Clear breakpoints in all files"), + _('Clear breakpoints in all files'), triggered=self.clear_all_breakpoints) - breakpoints_menu = QMenu(_("Breakpoints"), self) - add_actions(breakpoints_menu, (set_clear_breakpoint_action, - set_cond_breakpoint_action, None, - clear_all_breakpoints_action)) self.winpdb_action = create_action(self, _("Debug with winpdb"), triggered=self.run_winpdb) self.winpdb_action.setEnabled(WINPDB_PATH is not None and PY2) - self.register_shortcut(self.winpdb_action, context="Editor", - name="Debug with winpdb") - + # --- Debug toolbar --- - debug_action = create_action(self, _("&Debug"), icon='debug.png', + debug_action = create_action(self, _("&Debug"), + icon=ima.icon('debug'), tip=_("Debug file"), triggered=self.debug_file) - self.register_shortcut(debug_action, context="Editor", name="Debug") - add_shortcut_to_tooltip(debug_action, context="Editor", name="Debug") - - debug_next_action = create_action(self, _("Step"), - icon='arrow-step-over.png', tip=_("Run current line"), - triggered=lambda: self.debug_command("next")) - self.register_shortcut(debug_next_action, "_", "Debug Step Over") - add_shortcut_to_tooltip(debug_next_action, context="_", - name="Debug Step Over") + self.register_shortcut(debug_action, context="_", name="Debug", + add_sc_to_tip=True) + + debug_next_action = create_action(self, _("Step"), + icon=ima.icon('arrow-step-over'), tip=_("Run current line"), + triggered=lambda: self.debug_command("next")) + self.register_shortcut(debug_next_action, "_", "Debug Step Over", + add_sc_to_tip=True) debug_continue_action = create_action(self, _("Continue"), - icon='arrow-continue.png', tip=_("Continue execution until " - "next breakpoint"), - triggered=lambda: self.debug_command("continue")) - self.register_shortcut(debug_continue_action, "_", "Debug Continue") - add_shortcut_to_tooltip(debug_continue_action, context="_", - name="Debug Continue") - - debug_step_action = create_action(self, _("Step Into"), - icon='arrow-step-in.png', tip=_("Step into function or method " - "of current line"), - triggered=lambda: self.debug_command("step")) - self.register_shortcut(debug_step_action, "_", "Debug Step Into") - add_shortcut_to_tooltip(debug_step_action, context="_", - name="Debug Step Into") - - debug_return_action = create_action(self, _("Step Return"), - icon='arrow-step-out.png', tip=_("Run until current function " - "or method returns"), - triggered=lambda: self.debug_command("return")) - self.register_shortcut(debug_return_action, "_", "Debug Step Return") - add_shortcut_to_tooltip(debug_return_action, context="_", - name="Debug Step Return") - - debug_exit_action = create_action(self, _("Exit"), - icon='stop_debug.png', tip=_("Exit Debug"), - triggered=lambda: self.debug_command("exit")) - self.register_shortcut(debug_exit_action, "_", "Debug Exit") - add_shortcut_to_tooltip(debug_exit_action, context="_", - name="Debug Exit") - - debug_control_menu_actions = [debug_next_action, - debug_step_action, - debug_return_action, - debug_continue_action, - debug_exit_action] - debug_control_menu = QMenu(_("Debugging control")) - add_actions(debug_control_menu, debug_control_menu_actions) - + icon=ima.icon('arrow-continue'), + tip=_("Continue execution until next breakpoint"), + triggered=lambda: self.debug_command("continue")) + self.register_shortcut(debug_continue_action, "_", "Debug Continue", + add_sc_to_tip=True) + + debug_step_action = create_action(self, _("Step Into"), + icon=ima.icon('arrow-step-in'), + tip=_("Step into function or method of current line"), + triggered=lambda: self.debug_command("step")) + self.register_shortcut(debug_step_action, "_", "Debug Step Into", + add_sc_to_tip=True) + + debug_return_action = create_action(self, _("Step Return"), + icon=ima.icon('arrow-step-out'), + tip=_("Run until current function or method returns"), + triggered=lambda: self.debug_command("return")) + self.register_shortcut(debug_return_action, "_", "Debug Step Return", + add_sc_to_tip=True) + + debug_exit_action = create_action(self, _("Stop"), + icon=ima.icon('stop_debug'), tip=_("Stop debugging"), + triggered=lambda: self.debug_command("exit")) + self.register_shortcut(debug_exit_action, "_", "Debug Exit", + add_sc_to_tip=True) + # --- Run toolbar --- - run_action = create_action(self, _("&Run"), icon='run.png', + run_action = create_action(self, _("&Run"), icon=ima.icon('run'), tip=_("Run file"), triggered=self.run_file) - self.register_shortcut(run_action, context="Editor", name="Run") - add_shortcut_to_tooltip(run_action, context="Editor", name="Run") + self.register_shortcut(run_action, context="_", name="Run", + add_sc_to_tip=True) - configure_action = create_action(self, - _("&Configure..."), icon='run_settings.png', + configure_action = create_action(self, _("&Configure..."), + icon=ima.icon('run_settings'), tip=_("Run settings"), menurole=QAction.NoRole, triggered=self.edit_run_configurations) - self.register_shortcut(configure_action, context="Editor", - name="Configure") - add_shortcut_to_tooltip(configure_action, context="Editor", - name="Configure") - - re_run_action = create_action(self, - _("Re-run &last script"), icon='run_again.png', + self.register_shortcut(configure_action, context="_", + name="Configure", add_sc_to_tip=True) + + re_run_action = create_action(self, _("Re-run &last script"), + icon=ima.icon('run_again'), tip=_("Run again last file"), triggered=self.re_run_file) - self.register_shortcut(re_run_action, context="Editor", - name="Re-run last script") - add_shortcut_to_tooltip(re_run_action, context="Editor", - name="Re-run last script") + self.register_shortcut(re_run_action, context="_", + name="Re-run last script", + add_sc_to_tip=True) run_selected_action = create_action(self, _("Run &selection or " "current line"), - icon='run_selection.png', + icon=ima.icon('run_selection'), tip=_("Run selection or " "current line"), - triggered=self.run_selection) + triggered=self.run_selection, + context=Qt.WidgetShortcut) self.register_shortcut(run_selected_action, context="Editor", name="Run selection") - if sys.platform == 'darwin': - run_cell_sc = Qt.META + Qt.Key_Enter - else: - run_cell_sc = Qt.CTRL + Qt.Key_Enter - run_cell_advance_sc = Qt.SHIFT + Qt.Key_Enter - run_cell_action = create_action(self, - _("Run cell"), icon='run_cell.png', - shortcut=QKeySequence(run_cell_sc), + _("Run cell"), + icon=ima.icon('run_cell'), + shortcut=QKeySequence(RUN_CELL_SHORTCUT), tip=_("Run current cell (Ctrl+Enter)\n" "[Use #%% to create cells]"), - triggered=self.run_cell) + triggered=self.run_cell, + context=Qt.WidgetShortcut) run_cell_advance_action = create_action(self, - _("Run cell and advance"), - icon='run_cell_advance.png', - shortcut=QKeySequence(run_cell_advance_sc), - tip=_("Run current cell and go to " - "the next one (Shift+Enter)"), - triggered=self.run_cell_and_advance) - + _("Run cell and advance"), + icon=ima.icon('run_cell_advance'), + shortcut=QKeySequence(RUN_CELL_AND_ADVANCE_SHORTCUT), + tip=_("Run current cell and go to the next one " + "(Shift+Enter)"), + triggered=self.run_cell_and_advance, + context=Qt.WidgetShortcut) + # --- Source code Toolbar --- self.todo_list_action = create_action(self, - _("Show todo list"), icon='todo_list.png', + _("Show todo list"), icon=ima.icon('todo_list'), tip=_("Show TODO/FIXME/XXX/HINT/TIP/@todo comments list"), triggered=self.go_to_next_todo) self.todo_menu = QMenu(self) self.todo_list_action.setMenu(self.todo_menu) - self.connect(self.todo_menu, SIGNAL("aboutToShow()"), - self.update_todo_menu) + self.todo_menu.aboutToShow.connect(self.update_todo_menu) self.warning_list_action = create_action(self, - _("Show warning/error list"), icon='wng_list.png', + _("Show warning/error list"), icon=ima.icon('wng_list'), tip=_("Show code analysis warnings/errors"), triggered=self.go_to_next_warning) self.warning_menu = QMenu(self) self.warning_list_action.setMenu(self.warning_menu) - self.connect(self.warning_menu, SIGNAL("aboutToShow()"), - self.update_warning_menu) + self.warning_menu.aboutToShow.connect(self.update_warning_menu) self.previous_warning_action = create_action(self, - _("Previous warning/error"), icon='prev_wng.png', + _("Previous warning/error"), icon=ima.icon('prev_wng'), tip=_("Go to previous code analysis warning/error"), triggered=self.go_to_previous_warning) self.next_warning_action = create_action(self, - _("Next warning/error"), icon='next_wng.png', + _("Next warning/error"), icon=ima.icon('next_wng'), tip=_("Go to next code analysis warning/error"), triggered=self.go_to_next_warning) self.previous_edit_cursor_action = create_action(self, - _("Last edit location"), icon='last_edit_location.png', + _("Last edit location"), icon=ima.icon('last_edit_location'), tip=_("Go to last edit location"), - triggered=self.go_to_last_edit_location) + triggered=self.go_to_last_edit_location, + context=Qt.WidgetShortcut) self.register_shortcut(self.previous_edit_cursor_action, context="Editor", - name="Last edit location") + name="Last edit location", + add_sc_to_tip=True) self.previous_cursor_action = create_action(self, - _("Previous cursor position"), icon='prev_cursor.png', + _("Previous cursor position"), icon=ima.icon('prev_cursor'), tip=_("Go to previous cursor position"), - triggered=self.go_to_previous_cursor_position) + triggered=self.go_to_previous_cursor_position, + context=Qt.WidgetShortcut) self.register_shortcut(self.previous_cursor_action, - context="Editor", - name="Previous cursor position") + context="Editor", + name="Previous cursor position", + add_sc_to_tip=True) self.next_cursor_action = create_action(self, - _("Next cursor position"), icon='next_cursor.png', + _("Next cursor position"), icon=ima.icon('next_cursor'), tip=_("Go to next cursor position"), - triggered=self.go_to_next_cursor_position) + triggered=self.go_to_next_cursor_position, + context=Qt.WidgetShortcut) self.register_shortcut(self.next_cursor_action, - context="Editor", name="Next cursor position") - + context="Editor", + name="Next cursor position", + add_sc_to_tip=True) + # --- Edit Toolbar --- self.toggle_comment_action = create_action(self, - _("Comment")+"/"+_("Uncomment"), icon='comment.png', + _("Comment")+"/"+_("Uncomment"), icon=ima.icon('comment'), tip=_("Comment current line or selection"), triggered=self.toggle_comment, context=Qt.WidgetShortcut) self.register_shortcut(self.toggle_comment_action, context="Editor", @@ -861,13 +906,23 @@ # keyPressEvent handler (the shortcut is here only to inform user): # (context=Qt.WidgetShortcut -> disable shortcut for other widgets) self.indent_action = create_action(self, - _("Indent"), "Tab", icon='indent.png', + _("Indent"), "Tab", icon=ima.icon('indent'), tip=_("Indent current line or selection"), triggered=self.indent, context=Qt.WidgetShortcut) self.unindent_action = create_action(self, - _("Unindent"), "Shift+Tab", icon='unindent.png', + _("Unindent"), "Shift+Tab", icon=ima.icon('unindent'), tip=_("Unindent current line or selection"), triggered=self.unindent, context=Qt.WidgetShortcut) + + self.text_uppercase_action = create_action(self, + _("Toggle Uppercase"), "Ctrl+Shift+U", + tip=_("Change to uppercase current line or selection"), + triggered=self.text_uppercase, context=Qt.WidgetShortcut) + + self.text_lowercase_action = create_action(self, + _("Toggle Lowercase"), "Ctrl+U", + tip=_("Change to lowercase current line or selection"), + triggered=self.text_lowercase, context=Qt.WidgetShortcut) # ---------------------------------------------------------------------- self.win_eol_action = create_action(self, @@ -896,7 +951,7 @@ triggered=self.fix_indentation) gotoline_action = create_action(self, _("Go to line..."), - icon=get_icon("gotoline.png"), + icon=ima.icon('gotoline'), triggered=self.go_to_line, context=Qt.WidgetShortcut) self.register_shortcut(gotoline_action, context="Editor", @@ -904,7 +959,7 @@ workdir_action = create_action(self, _("Set console working directory"), - icon=get_std_icon('DirOpenIcon'), + icon=ima.icon('DirOpenIcon'), tip=_("Set current console (and file explorer) working " "directory to current script directory"), triggered=self.__set_workdir) @@ -915,30 +970,56 @@ self.clear_recent_action = create_action(self, _("Clear this list"), tip=_("Clear recent files list"), triggered=self.clear_recent_files) + + # ---- File menu/toolbar construction ---- self.recent_file_menu = QMenu(_("Open &recent"), self) - self.connect(self.recent_file_menu, SIGNAL("aboutToShow()"), - self.update_recent_file_menu) + self.recent_file_menu.aboutToShow.connect(self.update_recent_file_menu) + + file_menu_actions = [self.new_action, + None, + self.open_action, + self.recent_file_menu, + None, + None, + self.save_action, + self.save_all_action, + save_as_action, + self.file_switcher_action, + self.revert_action, + None, + print_preview_action, + self.print_action, + None, + self.close_action, + self.close_all_action, + None] - file_menu_actions = [self.new_action, self.open_action, - self.recent_file_menu, self.save_action, - self.save_all_action, save_as_action, - self.revert_action, - None, print_preview_action, self.print_action, - None, self.close_action, - self.close_all_action, None] self.main.file_menu_actions += file_menu_actions file_toolbar_actions = [self.new_action, self.open_action, - self.save_action, self.save_all_action] + self.save_action, self.save_all_action, + self.file_switcher_action] self.main.file_toolbar_actions += file_toolbar_actions - + + # ---- Find menu/toolbar construction ---- + self.main.search_menu_actions = [find_action, + find_next_action, + find_previous_action, + replace_action] + self.main.search_toolbar_actions = [find_action, + find_next_action, + replace_action] + + # ---- Edit menu/toolbar construction ---- self.edit_menu_actions = [self.toggle_comment_action, blockcomment_action, unblockcomment_action, - self.indent_action, self.unindent_action] + self.indent_action, self.unindent_action, + self.text_uppercase_action, self.text_lowercase_action] self.main.edit_menu_actions += [None]+self.edit_menu_actions edit_toolbar_actions = [self.toggle_comment_action, self.unindent_action, self.indent_action] self.main.edit_toolbar_actions += edit_toolbar_actions - + + # ---- Search menu/toolbar construction ---- self.search_menu_actions = [gotoline_action] self.main.search_menu_actions += self.search_menu_actions self.main.search_toolbar_actions += [gotoline_action] @@ -952,31 +1033,57 @@ run_cell_advance_action, re_run_action, configure_action] self.main.run_toolbar_actions += run_toolbar_actions - + # ---- Debug menu/toolbar construction ---- - # The breakpoints plugin is expecting that - # breakpoints_menu will be the first QMenu in debug_menu_actions - # If breakpoints_menu must be moved below another QMenu in the list - # please update the breakpoints plugin accordingly. - debug_menu_actions = [debug_action, breakpoints_menu, - debug_control_menu, None, self.winpdb_action] + # NOTE: 'list_breakpoints' is used by the breakpoints + # plugin to add its "List breakpoints" action to this + # menu + debug_menu_actions = [debug_action, + debug_next_action, + debug_step_action, + debug_return_action, + debug_continue_action, + debug_exit_action, + None, + set_clear_breakpoint_action, + set_cond_breakpoint_action, + clear_all_breakpoints_action, + 'list_breakpoints', + None, + self.winpdb_action] self.main.debug_menu_actions += debug_menu_actions debug_toolbar_actions = [debug_action, debug_next_action, debug_step_action, debug_return_action, debug_continue_action, debug_exit_action] self.main.debug_toolbar_actions += debug_toolbar_actions - - source_menu_actions = [eol_menu, self.showblanks_action, - trailingspaces_action, fixindentation_action] + + # ---- Source menu/toolbar construction ---- + source_menu_actions = [eol_menu, + self.showblanks_action, + trailingspaces_action, + fixindentation_action, + None, + self.todo_list_action, + self.warning_list_action, + self.previous_warning_action, + self.next_warning_action, + None, + self.previous_edit_cursor_action, + self.previous_cursor_action, + self.next_cursor_action] self.main.source_menu_actions += source_menu_actions - + source_toolbar_actions = [self.todo_list_action, - self.warning_list_action, self.previous_warning_action, - self.next_warning_action, None, - self.previous_edit_cursor_action, - self.previous_cursor_action, self.next_cursor_action] + self.warning_list_action, + self.previous_warning_action, + self.next_warning_action, + None, + self.previous_edit_cursor_action, + self.previous_cursor_action, + self.next_cursor_action] self.main.source_toolbar_actions += source_toolbar_actions - + + # ---- Dock widget and file dependent actions ---- self.dock_toolbar_actions = file_toolbar_actions + [None] + \ source_toolbar_actions + [None] + \ run_toolbar_actions + [None] + \ @@ -999,24 +1106,30 @@ def register_plugin(self): """Register plugin in Spyder's main window""" - self.connect(self.main, SIGNAL('restore_scrollbar_position()'), - self.restore_scrollbar_position) - self.connect(self.main.console, - SIGNAL("edit_goto(QString,int,QString)"), self.load) - self.connect(self, SIGNAL('exec_in_extconsole(QString,bool)'), - self.main.execute_in_external_console) - self.connect(self, SIGNAL('redirect_stdio(bool)'), - self.main.redirect_internalshell_stdio) - self.connect(self, SIGNAL("open_dir(QString)"), - self.main.workingdirectory.chdir) - self.set_inspector(self.main.inspector) + self.main.restore_scrollbar_position.connect( + self.restore_scrollbar_position) + self.main.console.edit_goto.connect(self.load) + self.exec_in_extconsole.connect(self.main.execute_in_external_console) + self.redirect_stdio.connect(self.main.redirect_internalshell_stdio) + self.open_dir.connect(self.main.workingdirectory.chdir) + self.set_help(self.main.help) if self.main.outlineexplorer is not None: self.set_outlineexplorer(self.main.outlineexplorer) editorstack = self.get_current_editorstack() if not editorstack.data: self.__load_temp_file() self.main.add_dockwidget(self) - + + def update_font(self): + """Update font from Preferences""" + font = self.get_plugin_font() + color_scheme = self.get_color_scheme() + for editorstack in self.editorstacks: + editorstack.set_default_font(font, color_scheme) + completion_size = CONF.get('main', 'completion/size') + for finfo in editorstack.data: + comp_widget = finfo.editor.completion_widget + comp_widget.setup_appearance(completion_size, font) #------ Focus tabwidget def __get_focus_editorstack(self): @@ -1046,12 +1159,11 @@ for win in [self]+self.editorwindows: if win.isAncestorOf(editorstack): self.set_last_focus_editorstack(win, editorstack) - - + #------ Handling editorstacks def register_editorstack(self, editorstack): self.editorstacks.append(editorstack) - self.register_widget_shortcuts("Editor", editorstack) + self.register_widget_shortcuts(editorstack) if self.isAncestorOf(editorstack): # editorstack is a child of the Editor plugin @@ -1060,26 +1172,23 @@ if self.outlineexplorer is not None: editorstack.set_outlineexplorer(self.outlineexplorer) editorstack.set_find_widget(self.find_widget) - self.connect(editorstack, SIGNAL('reset_statusbar()'), - self.readwrite_status.hide) - self.connect(editorstack, SIGNAL('reset_statusbar()'), - self.encoding_status.hide) - self.connect(editorstack, SIGNAL('reset_statusbar()'), - self.cursorpos_status.hide) - self.connect(editorstack, SIGNAL('readonly_changed(bool)'), - self.readwrite_status.readonly_changed) - self.connect(editorstack, SIGNAL('encoding_changed(QString)'), - self.encoding_status.encoding_changed) - self.connect(editorstack, - SIGNAL('editor_cursor_position_changed(int,int)'), - self.cursorpos_status.cursor_position_changed) - self.connect(editorstack, SIGNAL('refresh_eol_chars(QString)'), - self.eol_status.eol_changed) - - editorstack.set_inspector(self.inspector) + editorstack.reset_statusbar.connect(self.readwrite_status.hide) + editorstack.reset_statusbar.connect(self.encoding_status.hide) + editorstack.reset_statusbar.connect(self.cursorpos_status.hide) + editorstack.readonly_changed.connect( + self.readwrite_status.readonly_changed) + editorstack.encoding_changed.connect( + self.encoding_status.encoding_changed) + editorstack.sig_editor_cursor_position_changed.connect( + self.cursorpos_status.cursor_position_changed) + editorstack.refresh_eol_chars.connect(self.eol_status.eol_changed) + + editorstack.set_help(self.help) editorstack.set_io_actions(self.new_action, self.open_action, self.save_action, self.revert_action) editorstack.set_tempfile_path(self.TEMPFILE_PATH) + editorstack.set_introspector(self.introspector) + settings = ( ('set_pyflakes_enabled', 'code_analysis/pyflakes'), ('set_pep8_enabled', 'code_analysis/pep8'), @@ -1107,8 +1216,8 @@ ('set_intelligent_backspace_enabled', 'intelligent_backspace'), ('set_highlight_current_line_enabled', 'highlight_current_line'), ('set_highlight_current_cell_enabled', 'highlight_current_cell'), - ('set_occurence_highlighting_enabled', 'occurence_highlighting'), - ('set_occurence_highlighting_timeout', 'occurence_highlighting/timeout'), + ('set_occurrence_highlighting_enabled', 'occurrence_highlighting'), + ('set_occurrence_highlighting_timeout', 'occurrence_highlighting/timeout'), ('set_checkeolchars_enabled', 'check_eol_chars'), ('set_fullpath_sorting_enabled', 'fullpath_sorting'), ('set_tabbar_visible', 'show_tab_bar'), @@ -1116,77 +1225,58 @@ ) for method, setting in settings: getattr(editorstack, method)(self.get_option(setting)) - editorstack.set_inspector_enabled(CONF.get('inspector', - 'connect/editor')) - color_scheme = get_color_scheme(self.get_option('color_scheme_name')) + editorstack.set_help_enabled(CONF.get('help', 'connect/editor')) + color_scheme = self.get_color_scheme() editorstack.set_default_font(self.get_plugin_font(), color_scheme) - - self.connect(editorstack, SIGNAL('starting_long_process(QString)'), - self.starting_long_process) - self.connect(editorstack, SIGNAL('ending_long_process(QString)'), - self.ending_long_process) - + + editorstack.starting_long_process.connect(self.starting_long_process) + editorstack.ending_long_process.connect(self.ending_long_process) + # Redirect signals - self.connect(editorstack, SIGNAL('redirect_stdio(bool)'), - lambda state: - self.emit(SIGNAL('redirect_stdio(bool)'), state)) - self.connect(editorstack, SIGNAL('exec_in_extconsole(QString,bool)'), - lambda text, option: self.emit( - SIGNAL('exec_in_extconsole(QString,bool)'), text, option)) - self.connect(editorstack, SIGNAL("update_plugin_title()"), - lambda: self.emit(SIGNAL("update_plugin_title()"))) - - self.connect(editorstack, SIGNAL("editor_focus_changed()"), - self.save_focus_editorstack) - self.connect(editorstack, SIGNAL('editor_focus_changed()'), - self.main.plugin_focus_changed) - - self.connect(editorstack, SIGNAL('zoom_in()'), lambda: self.zoom(1)) - self.connect(editorstack, SIGNAL('zoom_out()'), lambda: self.zoom(-1)) - self.connect(editorstack, SIGNAL('zoom_reset()'), lambda: self.zoom(0)) - self.connect(editorstack, SIGNAL('sig_new_file()'), self.new) - - self.connect(editorstack, SIGNAL('close_file(QString,int)'), - self.close_file_in_all_editorstacks) - self.connect(editorstack, SIGNAL('file_saved(QString,int,QString)'), - self.file_saved_in_editorstack) - self.connect(editorstack, - SIGNAL('file_renamed_in_data(QString,int,QString)'), - self.file_renamed_in_data_in_editorstack) - - self.connect(editorstack, SIGNAL("create_new_window()"), - self.create_new_window) - - self.connect(editorstack, SIGNAL('opened_files_list_changed()'), - self.opened_files_list_changed) - self.connect(editorstack, SIGNAL('analysis_results_changed()'), - self.analysis_results_changed) - self.connect(editorstack, SIGNAL('todo_results_changed()'), - self.todo_results_changed) - self.connect(editorstack, SIGNAL('update_code_analysis_actions()'), - self.update_code_analysis_actions) - self.connect(editorstack, SIGNAL('update_code_analysis_actions()'), - self.update_todo_actions) - self.connect(editorstack, - SIGNAL('refresh_file_dependent_actions()'), - self.refresh_file_dependent_actions) - self.connect(editorstack, SIGNAL('refresh_save_all_action()'), - self.refresh_save_all_action) - self.connect(editorstack, SIGNAL('refresh_eol_chars(QString)'), - self.refresh_eol_chars) - - self.connect(editorstack, SIGNAL("save_breakpoints(QString,QString)"), - self.save_breakpoints) - - self.connect(editorstack, SIGNAL('text_changed_at(QString,int)'), - self.text_changed_at) - self.connect(editorstack, SIGNAL('current_file_changed(QString,int)'), - self.current_file_changed) - - self.connect(editorstack, SIGNAL('plugin_load(QString)'), self.load) - self.connect(editorstack, SIGNAL("edit_goto(QString,int,QString)"), - self.load) - + editorstack.redirect_stdio.connect( + lambda state: self.redirect_stdio.emit(state)) + editorstack.exec_in_extconsole.connect( + lambda text, option: + self.exec_in_extconsole.emit(text, option)) + editorstack.update_plugin_title.connect( + lambda: self.update_plugin_title.emit()) + editorstack.editor_focus_changed.connect(self.save_focus_editorstack) + editorstack.editor_focus_changed.connect(self.set_editorstack_for_introspection) + editorstack.editor_focus_changed.connect(self.main.plugin_focus_changed) + editorstack.zoom_in.connect(lambda: self.zoom(1)) + editorstack.zoom_out.connect(lambda: self.zoom(-1)) + editorstack.zoom_reset.connect(lambda: self.zoom(0)) + editorstack.sig_new_file.connect(lambda s: self.new(text=s)) + editorstack.sig_new_file[()].connect(self.new) + editorstack.sig_close_file.connect(self.close_file_in_all_editorstacks) + editorstack.file_saved.connect(self.file_saved_in_editorstack) + editorstack.file_renamed_in_data.connect( + self.file_renamed_in_data_in_editorstack) + editorstack.create_new_window.connect(self.create_new_window) + editorstack.opened_files_list_changed.connect( + self.opened_files_list_changed) + editorstack.analysis_results_changed.connect( + self.analysis_results_changed) + editorstack.todo_results_changed.connect(self.todo_results_changed) + editorstack.update_code_analysis_actions.connect( + self.update_code_analysis_actions) + editorstack.update_code_analysis_actions.connect( + self.update_todo_actions) + editorstack.refresh_file_dependent_actions.connect( + self.refresh_file_dependent_actions) + editorstack.refresh_save_all_action.connect(self.refresh_save_all_action) + editorstack.refresh_eol_chars.connect(self.refresh_eol_chars) + editorstack.save_breakpoints.connect(self.save_breakpoints) + editorstack.text_changed_at.connect(self.text_changed_at) + editorstack.current_file_changed.connect(self.current_file_changed) + editorstack.plugin_load.connect(self.load) + editorstack.plugin_load[()].connect(self.load) + editorstack.edit_goto.connect(self.load) + editorstack.sig_save_as.connect(self.save_as) + editorstack.sig_prev_edit_pos.connect(self.go_to_last_edit_location) + editorstack.sig_prev_cursor.connect(self.go_to_previous_cursor_position) + editorstack.sig_next_cursor.connect(self.go_to_next_cursor_position) + def unregister_editorstack(self, editorstack): """Removing editorstack only if it's not the last remaining""" self.remove_last_focus_editorstack(editorstack) @@ -1201,24 +1291,24 @@ def clone_editorstack(self, editorstack): editorstack.clone_from(self.editorstacks[0]) for finfo in editorstack.data: - self.register_widget_shortcuts("Editor", finfo.editor) - - @Slot(int, int) + self.register_widget_shortcuts(finfo.editor) + + @Slot(str, int) def close_file_in_all_editorstacks(self, editorstack_id_str, index): for editorstack in self.editorstacks: if str(id(editorstack)) != editorstack_id_str: editorstack.blockSignals(True) editorstack.close_file(index, force=True) editorstack.blockSignals(False) - - @Slot(int, int) + + @Slot(str, int, str) def file_saved_in_editorstack(self, editorstack_id_str, index, filename): """A file was saved in editorstack, this notifies others""" for editorstack in self.editorstacks: if str(id(editorstack)) != editorstack_id_str: editorstack.file_saved_in_other_editorstack(index, filename) - @Slot(int, int) + @Slot(str, int, str) def file_renamed_in_data_in_editorstack(self, editorstack_id_str, index, filename): """A file was renamed in data in editorstack, this notifies others""" @@ -1226,8 +1316,29 @@ if str(id(editorstack)) != editorstack_id_str: editorstack.rename_in_data(index, filename) + def set_editorstack_for_introspection(self): + """ + Set the current editorstack to be used by the IntrospectionManager + instance + """ + editorstack = self.__get_focus_editorstack() + if editorstack is not None: + self.introspector.set_editor_widget(editorstack) - #------ Handling editor windows + # Disconnect active signals + try: + self.introspector.send_to_help.disconnect() + self.introspector.edit_goto.disconnect() + except TypeError: + pass + + # Reconnect signals again + self.introspector.send_to_help.connect(editorstack.send_to_help) + self.introspector.edit_goto.connect( + lambda fname, lineno, name: + editorstack.edit_goto.emit(fname, lineno, name)) + + #------ Handling editor windows def setup_other_windows(self): """Setup toolbars and menus for 'New window' instances""" self.toolbar_list = ( @@ -1264,8 +1375,7 @@ window.resize(self.size()) window.show() self.register_editorwindow(window) - self.connect(window, SIGNAL("destroyed()"), - lambda win=window: self.unregister_editorwindow(win)) + window.destroyed.connect(lambda: self.unregister_editorwindow(window)) return window def register_editorwindow(self, window): @@ -1330,11 +1440,10 @@ action.setEnabled(enable) def refresh_save_all_action(self): - state = False - editorstack = self.editorstacks[0] - if editorstack.get_stack_count() > 1: - state = state or any([finfo.editor.document().isModified() - for finfo in editorstack.data]) + """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) def update_warning_menu(self): @@ -1346,8 +1455,12 @@ for message, line_number in check_results: error = 'syntax' in message text = message[:1].upper()+message[1:] - icon = get_icon('error.png' if error else 'warning.png') - slot = lambda _l=line_number: self.load(filename, goto=_l) + icon = ima.icon('error') if error else ima.icon('warning') + # QAction.triggered works differently for PySide and PyQt + if not API == 'pyside': + slot = lambda _checked, _l=line_number: self.load(filename, goto=_l) + else: + slot = lambda _l=line_number: self.load(filename, goto=_l) action = create_action(self, text=text, icon=icon, triggered=slot) self.warning_menu.addAction(action) @@ -1372,8 +1485,12 @@ self.todo_menu.clear() filename = self.get_current_filename() for text, line0 in results: - icon = get_icon('todo.png') - slot = lambda _l=line0: self.load(filename, goto=_l) + icon = ima.icon('todo') + # QAction.triggered works differently for PySide and PyQt + if not API == 'pyside': + slot = lambda _checked, _l=line0: self.load(filename, goto=_l) + else: + slot = lambda _l=line0: self.load(filename, goto=_l) action = create_action(self, text=text, icon=icon, triggered=slot) self.todo_menu.addAction(action) self.update_todo_actions() @@ -1458,7 +1575,7 @@ else: breakpoints = [] save_breakpoints(filename, breakpoints) - self.emit(SIGNAL("breakpoints_saved()")) + self.breakpoints_saved.emit() #------ File I/O def __load_temp_file(self): @@ -1474,12 +1591,13 @@ encoding.write(to_text_string(text), self.TEMPFILE_PATH, 'utf-8') self.load(self.TEMPFILE_PATH) + @Slot() def __set_workdir(self): """Set current script directory as working directory""" fname = self.get_current_filename() if fname is not None: directory = osp.dirname(osp.abspath(fname)) - self.emit(SIGNAL("open_dir(QString)"), directory) + self.open_dir.emit(directory) def __add_recent_file(self, fname): """Add to recent file list""" @@ -1497,8 +1615,10 @@ is created (when loading or creating a new file)""" for editorstack in self.editorstacks[1:]: editor = editorstack.clone_editor_from(finfo, set_current=False) - self.register_widget_shortcuts("Editor", editor) - + self.register_widget_shortcuts(editor) + + @Slot() + @Slot(str) def new(self, fname=None, editorstack=None, text=None): """ Create a new file - Untitled @@ -1566,14 +1686,19 @@ finfo.path = self.main.get_spyder_pythonpath() self._clone_file_everywhere(finfo) current_editor = current_es.set_current_filename(finfo.filename) - self.register_widget_shortcuts("Editor", current_editor) + self.register_widget_shortcuts(current_editor) if not created_from_here: self.save(force=True) def edit_template(self): """Edit new file template""" self.load(self.TEMPLATE_PATH) - + + @Slot() + def call_file_switcher(self): + if self.editorstacks: + self.get_current_editorstack().open_fileswitcher_dlg() + def update_recent_file_menu(self): """Update recent file menu""" recent_files = [] @@ -1582,33 +1707,35 @@ recent_files.append(fname) self.recent_file_menu.clear() if recent_files: - for i, fname in enumerate(recent_files): - if i < 10: - accel = "%d" % ((i+1) % 10) - else: - accel = chr(i-10+ord('a')) - action = create_action(self, "&%s %s" % (accel, fname), - icon=get_filetype_icon(fname), + for fname in recent_files: + action = create_action(self, fname, + icon=ima.icon('FileIcon'), triggered=self.load) action.setData(to_qvariant(fname)) self.recent_file_menu.addAction(action) self.clear_recent_action.setEnabled(len(recent_files) > 0) add_actions(self.recent_file_menu, (None, self.max_recent_action, self.clear_recent_action)) - + + @Slot() def clear_recent_files(self): """Clear recent files list""" self.recent_files = [] - + + @Slot() def change_max_recent_files(self): "Change max recent files entries""" editorstack = self.get_current_editorstack() - mrf, valid = QInputDialog.getInteger(editorstack, _('Editor'), + mrf, valid = QInputDialog.getInt(editorstack, _('Editor'), _('Maximum number of recent files'), self.get_option('max_recent_files'), 1, 35) if valid: self.set_option('max_recent_files', mrf) - + + @Slot() + @Slot(str) + @Slot(str, int, str) + @Slot(str, int, str, object) def load(self, filenames=None, goto=None, word='', editorwindow=None, processevents=True): """ @@ -1632,26 +1759,32 @@ filenames = from_qvariant(action.data(), to_text_string) if not filenames: basedir = getcwd() + if self.edit_filetypes is None: + self.edit_filetypes = get_edit_filetypes() + if self.edit_filters is None: + self.edit_filters = get_edit_filters() if CONF.get('workingdir', 'editor/open/browse_scriptdir'): c_fname = self.get_current_filename() if c_fname is not None and c_fname != self.TEMPFILE_PATH: basedir = osp.dirname(c_fname) - self.emit(SIGNAL('redirect_stdio(bool)'), False) + self.redirect_stdio.emit(False) parent_widget = self.get_current_editorstack() if filename0 is not None: - selectedfilter = get_filter(EDIT_FILETYPES, + selectedfilter = get_filter(self.edit_filetypes, osp.splitext(filename0)[1]) else: selectedfilter = '' - filenames, _selfilter = getopenfilenames(parent_widget, - _("Open file"), basedir, EDIT_FILTERS, - selectedfilter=selectedfilter) - self.emit(SIGNAL('redirect_stdio(bool)'), True) + filenames, _sf = getopenfilenames(parent_widget, + _("Open file"), basedir, + self.edit_filters, + selectedfilter=selectedfilter, + options=QFileDialog.HideNameFilterDetails) + self.redirect_stdio.emit(True) if filenames: filenames = [osp.normpath(fname) for fname in filenames] if CONF.get('workingdir', 'editor/open/auto_set_to_basedir'): directory = osp.dirname(filenames[0]) - self.emit(SIGNAL("open_dir(QString)"), directory) + self.open_dir.emit(directory) else: return @@ -1699,8 +1832,8 @@ self._clone_file_everywhere(finfo) current_editor = current_es.set_current_filename(filename) current_editor.set_breakpoints(load_breakpoints(filename)) - self.register_widget_shortcuts("Editor", current_editor) - + self.register_widget_shortcuts(current_editor) + current_es.analyze_script() self.__add_recent_file(filename) if goto is not None: # 'word' is assumed to be None as well @@ -1713,6 +1846,7 @@ if processevents: QApplication.processEvents() + @Slot() def print_file(self): """Print current file""" editor = self.get_current_editor() @@ -1722,62 +1856,93 @@ printDialog = QPrintDialog(printer, editor) if editor.has_selected_text(): printDialog.addEnabledOption(QAbstractPrintDialog.PrintSelection) - self.emit(SIGNAL('redirect_stdio(bool)'), False) + self.redirect_stdio.emit(False) answer = printDialog.exec_() - self.emit(SIGNAL('redirect_stdio(bool)'), True) + self.redirect_stdio.emit(True) if answer == QDialog.Accepted: self.starting_long_process(_("Printing...")) printer.setDocName(filename) editor.print_(printer) self.ending_long_process() + @Slot() def print_preview(self): """Print preview for current file""" - from spyderlib.qt.QtGui import QPrintPreviewDialog + from qtpy.QtPrintSupport import QPrintPreviewDialog + editor = self.get_current_editor() printer = Printer(mode=QPrinter.HighResolution, header_font=self.get_plugin_font('printer_header')) preview = QPrintPreviewDialog(printer, self) preview.setWindowFlags(Qt.Window) - self.connect(preview, SIGNAL("paintRequested(QPrinter*)"), - lambda printer: editor.print_(printer)) - self.emit(SIGNAL('redirect_stdio(bool)'), False) + preview.paintRequested.connect(lambda printer: editor.print_(printer)) + self.redirect_stdio.emit(False) preview.exec_() - self.emit(SIGNAL('redirect_stdio(bool)'), True) + self.redirect_stdio.emit(True) + @Slot() def close_file(self): """Close current file""" editorstack = self.get_current_editorstack() editorstack.close_file() + @Slot() def close_all_files(self): """Close all opened scripts""" self.editorstacks[0].close_all_files() - + + @Slot() def save(self, index=None, force=False): """Save file""" editorstack = self.get_current_editorstack() return editorstack.save(index=index, force=force) - + + @Slot() def save_as(self): """Save *as* the currently edited file""" editorstack = self.get_current_editorstack() if editorstack.save_as(): fname = editorstack.get_current_filename() if CONF.get('workingdir', 'editor/save/auto_set_to_basedir'): - self.emit(SIGNAL("open_dir(QString)"), osp.dirname(fname)) + self.open_dir.emit(osp.dirname(fname)) self.__add_recent_file(fname) - + + @Slot() def save_all(self): """Save all opened files""" self.get_current_editorstack().save_all() - + + @Slot() def revert(self): """Revert the currently edited file from disk""" editorstack = self.get_current_editorstack() editorstack.revert() - - + + @Slot() + def find(self): + """Find slot""" + editorstack = self.get_current_editorstack() + editorstack.find_widget.show() + editorstack.find_widget.search_text.setFocus() + + @Slot() + def find_next(self): + """Fnd next slot""" + editorstack = self.get_current_editorstack() + editorstack.find_widget.find_next() + + @Slot() + def find_previous(self): + """Find previous slot""" + editorstack = self.get_current_editorstack() + editorstack.find_widget.find_previous() + + @Slot() + def replace(self): + """Replace slot""" + editorstack = self.get_current_editorstack() + editorstack.find_widget.show_replace() + #------ Explorer widget def close_file_from_name(self, filename): """Close file from its name""" @@ -1795,7 +1960,7 @@ dirname = osp.abspath(to_text_string(dirname)) for fname in self.get_filenames(): if osp.abspath(fname).startswith(dirname): - self.__close(fname) + self.close_file_from_name(fname) def renamed(self, source, dest): """File was renamed in file explorer widget or in project explorer""" @@ -1808,54 +1973,76 @@ #------ Source code + @Slot() def indent(self): """Indent current line or selection""" editor = self.get_current_editor() if editor is not None: editor.indent() + @Slot() def unindent(self): """Unindent current line or selection""" editor = self.get_current_editor() if editor is not None: editor.unindent() - + + @Slot() + def text_uppercase (self): + """Change current line or selection to uppercase.""" + editor = self.get_current_editor() + if editor is not None: + editor.transform_to_uppercase() + + @Slot() + def text_lowercase(self): + """Change current line or selection to lowercase.""" + editor = self.get_current_editor() + if editor is not None: + editor.transform_to_lowercase() + + @Slot() def toggle_comment(self): """Comment current line or selection""" editor = self.get_current_editor() if editor is not None: editor.toggle_comment() - + + @Slot() def blockcomment(self): """Block comment current line or selection""" editor = self.get_current_editor() if editor is not None: editor.blockcomment() + @Slot() def unblockcomment(self): """Un-block comment current line or selection""" editor = self.get_current_editor() if editor is not None: editor.unblockcomment() - + @Slot() def go_to_next_todo(self): editor = self.get_current_editor() position = editor.go_to_next_todo() filename = self.get_current_filename() self.add_cursor_position_to_history(filename, position) - + + @Slot() def go_to_next_warning(self): editor = self.get_current_editor() position = editor.go_to_next_warning() filename = self.get_current_filename() self.add_cursor_position_to_history(filename, position) - + + @Slot() def go_to_previous_warning(self): editor = self.get_current_editor() position = editor.go_to_previous_warning() filename = self.get_current_filename() self.add_cursor_position_to_history(filename, position) - + + @Slot() def run_winpdb(self): """Run winpdb to debug current file""" if self.save(): @@ -1867,25 +2054,28 @@ else: args = runconf.get_arguments().split() wdir = runconf.get_working_directory() - # Handle the case where wdir comes back as an empty string - # when the working directory dialog checkbox is unchecked. - if not wdir: - wdir = None - programs.run_program(WINPDB_PATH, [fname]+args, wdir) + # Handle the case where wdir comes back as an empty string + # when the working directory dialog checkbox is unchecked. + # (subprocess "cwd" default is None, so empty str + # must be changed to None in this case.) + programs.run_program(WINPDB_PATH, [fname] + args, cwd=wdir or None) def toggle_eol_chars(self, os_name): editor = self.get_current_editor() if self.__set_eol_chars: editor.set_eol_chars(sourcecode.get_eol_chars_from_os_name(os_name)) - + + @Slot(bool) def toggle_show_blanks(self, checked): editor = self.get_current_editor() editor.set_blanks_enabled(checked) - + + @Slot() def remove_trailing_spaces(self): editorstack = self.get_current_editorstack() editorstack.remove_trailing_spaces() - + + @Slot() def fix_indentation(self): editorstack = self.get_current_editorstack() editorstack.fix_indentation() @@ -1934,7 +2124,8 @@ def current_file_changed(self, filename, position): self.add_cursor_position_to_history(to_text_string(filename), position, fc=True) - + + @Slot() def go_to_last_edit_location(self): if self.last_edit_cursor_pos is not None: filename, position = self.last_edit_cursor_pos @@ -1972,35 +2163,41 @@ editor.set_cursor_position(position) self.__ignore_cursor_position = False self.update_cursorpos_actions() - + + @Slot() def go_to_previous_cursor_position(self): self.__move_cursor_position(-1) - + + @Slot() def go_to_next_cursor_position(self): self.__move_cursor_position(1) - + + @Slot() def go_to_line(self): """Open 'go to line' dialog""" editorstack = self.get_current_editorstack() if editorstack is not None: editorstack.go_to_line() - + + @Slot() def set_or_clear_breakpoint(self): """Set/Clear breakpoint""" editorstack = self.get_current_editorstack() if editorstack is not None: editorstack.set_or_clear_breakpoint() - + + @Slot() def set_or_edit_conditional_breakpoint(self): """Set/Edit conditional breakpoint""" editorstack = self.get_current_editorstack() if editorstack is not None: editorstack.set_or_edit_conditional_breakpoint() - + + @Slot() def clear_all_breakpoints(self): """Clear breakpoints in all files""" clear_all_breakpoints() - self.emit(SIGNAL("breakpoints_saved()")) + self.breakpoints_saved.emit() editorstack = self.get_current_editorstack() if editorstack is not None: for data in editorstack.data: @@ -2010,7 +2207,7 @@ def clear_breakpoint(self, filename, lineno): """Remove a single breakpoint""" clear_breakpoint(filename, lineno) - self.emit(SIGNAL("breakpoints_saved()")) + self.breakpoints_saved.emit() editorstack = self.get_current_editorstack() if editorstack is not None: index = self.is_file_opened(filename) @@ -2021,20 +2218,20 @@ """Debug actions""" if self.main.ipyconsole is not None: if self.main.last_console_plugin_focus_was_python: - self.main.extconsole.execute_python_code(command) + self.main.extconsole.execute_code(command) else: self.main.ipyconsole.write_to_stdin(command) focus_widget = self.main.ipyconsole.get_focus_widget() if focus_widget: focus_widget.setFocus() else: - self.main.extconsole.execute_python_code(command) + self.main.extconsole.execute_code(command) #------ Run Python script + @Slot() def edit_run_configurations(self): dialog = RunConfigDialog(self) - self.connect(dialog, SIGNAL("size_change(QSize)"), - lambda s: self.set_dialog_size(s)) + dialog.size_change.connect(lambda s: self.set_dialog_size(s)) if self.dialog_size is not None: dialog.resize(self.dialog_size) fname = osp.abspath(self.get_current_filename()) @@ -2044,7 +2241,8 @@ if fname is not None: self.load(fname) self.run_file() - + + @Slot() def run_file(self, debug=False): """Run script inside current interpreter or in a new one""" editorstack = self.get_current_editorstack() @@ -2059,8 +2257,7 @@ runconf = get_run_configuration(fname) if runconf is None: dialog = RunConfigOneDialog(self) - self.connect(dialog, SIGNAL("size_change(QSize)"), - lambda s: self.set_dialog_size(s)) + dialog.size_change.connect(lambda s: self.set_dialog_size(s)) if self.dialog_size is not None: dialog.resize(self.dialog_size) dialog.setup(fname) @@ -2082,13 +2279,14 @@ args = runconf.get_arguments() python_args = runconf.get_python_arguments() interact = runconf.interact + post_mortem = runconf.post_mortem current = runconf.current systerm = runconf.systerm python = True # Note: in the future, it may be useful to run # something in a terminal instead of a Python interp. self.__last_ec_exec = (fname, wdir, args, interact, debug, - python, python_args, current, systerm) + python, python_args, current, systerm, post_mortem) self.re_run_file() if not interact and not debug: # If external console dockwidget is hidden, it will be @@ -2100,14 +2298,19 @@ def set_dialog_size(self, size): self.dialog_size = size + @Slot() def debug_file(self): """Debug current script""" self.run_file(debug=True) - editor = self.get_current_editor() - if editor.get_breakpoints(): - time.sleep(0.5) - self.debug_command('continue') - + # Fixes 2034 + # FIXME: Stop doing this for now because it breaks debugging + # for IPython consoles + #editor = self.get_current_editor() + #if editor.get_breakpoints(): + # time.sleep(0.5) + # self.debug_command('continue') + + @Slot() def re_run_file(self): """Re-run last script""" if self.get_option('save_all_before_run'): @@ -2115,36 +2318,36 @@ if self.__last_ec_exec is None: return (fname, wdir, args, interact, debug, - python, python_args, current, systerm) = self.__last_ec_exec + python, python_args, current, systerm, post_mortem) = self.__last_ec_exec if current: if self.main.ipyconsole is not None: if self.main.last_console_plugin_focus_was_python: - self.emit( - SIGNAL('run_in_current_extconsole(QString,QString,QString,bool)'), - fname, wdir, args, debug) + self.run_in_current_extconsole.emit(fname, wdir, args, + debug, post_mortem) else: - self.emit( - SIGNAL('run_in_current_ipyclient(QString,QString,QString,bool)'), - fname, wdir, args, debug) + self.run_in_current_ipyclient.emit(fname, wdir, args, + debug, post_mortem) else: - self.emit( - SIGNAL('run_in_current_extconsole(QString,QString,QString,bool)'), - fname, wdir, args, debug) + self.run_in_current_extconsole.emit(fname, wdir, args, debug, + post_mortem) else: self.main.open_external_console(fname, wdir, args, interact, debug, python, python_args, - systerm) + systerm, post_mortem) + @Slot() def run_selection(self): """Run selection or current line in external console""" editorstack = self.get_current_editorstack() editorstack.run_selection() - + + @Slot() def run_cell(self): """Run current cell""" editorstack = self.get_current_editorstack() editorstack.run_cell() + @Slot() def run_cell_and_advance(self): """Run current cell and advance to the next one""" editorstack = self.get_current_editorstack() @@ -2171,30 +2374,20 @@ if self.editorstacks is not None: # --- syntax highlight and text rendering settings color_scheme_n = 'color_scheme_name' - color_scheme_o = get_color_scheme(self.get_option(color_scheme_n)) - font_n = 'plugin_font' - font_o = self.get_plugin_font() + color_scheme_o = self.get_color_scheme() currentline_n = 'highlight_current_line' currentline_o = self.get_option(currentline_n) currentcell_n = 'highlight_current_cell' currentcell_o = self.get_option(currentcell_n) - occurence_n = 'occurence_highlighting' - occurence_o = self.get_option(occurence_n) - occurence_timeout_n = 'occurence_highlighting/timeout' - occurence_timeout_o = self.get_option(occurence_timeout_n) + occurrence_n = 'occurrence_highlighting' + occurrence_o = self.get_option(occurrence_n) + occurrence_timeout_n = 'occurrence_highlighting/timeout' + occurrence_timeout_o = self.get_option(occurrence_timeout_n) focus_to_editor_n = 'focus_to_editor' focus_to_editor_o = self.get_option(focus_to_editor_n) for editorstack in self.editorstacks: - if font_n in options: - scs = color_scheme_o if color_scheme_n in options else None - editorstack.set_default_font(font_o, scs) - completion_size = CONF.get('editor_appearance', - 'completion/size') - for finfo in editorstack.data: - comp_widget = finfo.editor.completion_widget - comp_widget.setup_appearance(completion_size, font_o) - elif color_scheme_n in options: + if color_scheme_n in options: editorstack.set_color_scheme(color_scheme_o) if currentline_n in options: editorstack.set_highlight_current_line_enabled( @@ -2202,11 +2395,11 @@ if currentcell_n in options: editorstack.set_highlight_current_cell_enabled( currentcell_o) - if occurence_n in options: - editorstack.set_occurence_highlighting_enabled(occurence_o) - if occurence_timeout_n in options: - editorstack.set_occurence_highlighting_timeout( - occurence_timeout_o) + if occurrence_n in options: + editorstack.set_occurrence_highlighting_enabled(occurrence_o) + if occurrence_timeout_n in options: + editorstack.set_occurrence_highlighting_timeout( + occurrence_timeout_o) if focus_to_editor_n in options: editorstack.set_focus_to_editor(focus_to_editor_o) @@ -2253,8 +2446,8 @@ indent_chars_o = self.get_option(indent_chars_n) tab_stop_width_n = 'tab_stop_width' tab_stop_width_o = self.get_option(tab_stop_width_n) - inspector_n = 'connect_to_oi' - inspector_o = CONF.get('inspector', 'connect/editor') + help_n = 'connect_to_oi' + help_o = CONF.get('help', 'connect/editor') todo_n = 'todo_list' todo_o = self.get_option(todo_n) pyflakes_n = 'code_analysis/pyflakes' @@ -2317,8 +2510,8 @@ editorstack.set_indent_chars(indent_chars_o) if tab_stop_width_n in options: editorstack.set_tab_stop_width(tab_stop_width_o) - if inspector_n in options: - editorstack.set_inspector_enabled(inspector_o) + if help_n in options: + editorstack.set_help_enabled(help_o) if todo_n in options: editorstack.set_todolist_enabled(todo_o, current_finfo=finfo) @@ -2339,3 +2532,59 @@ finfo.run_todo_finder() if pyflakes_n in options or pep8_n in options: finfo.run_code_analysis(pyflakes_o, pep8_o) + + # --- Open files + def get_open_filenames(self): + """Get the list of open files in the current stack""" + editorstack = self.editorstacks[0] + filenames = [] + filenames += [finfo.filename for finfo in editorstack.data] + return filenames + + def set_open_filenames(self): + """ + Set the recent opened files on editor based on active project. + + If no project is active, then editor filenames are saved, otherwise + the opened filenames are stored in the project config info. + """ + if self.projects is not None: + if not self.projects.get_active_project(): + filenames = self.get_open_filenames() + self.set_option('filenames', filenames) + + def setup_open_files(self): + """Open the list of saved files per project""" + self.set_create_new_file_if_empty(False) + active_project_path = None + if self.projects is not None: + active_project_path = self.projects.get_active_project_path() + + if active_project_path: + filenames = self.projects.get_project_filenames() + else: + filenames = self.get_option('filenames', default=[]) + self.close_all_files() + + if filenames and any([osp.isfile(f) for f in filenames]): + self.load(filenames) + layout = self.get_option('layout_settings', None) + if layout is not None: + self.editorsplitter.set_layout_settings(layout) + win_layout = self.get_option('windows_layout_settings', None) + if win_layout: + for layout_settings in win_layout: + self.editorwindows_to_be_created.append(layout_settings) + self.set_last_focus_editorstack(self, self.editorstacks[0]) + else: + self.__load_temp_file() + self.set_create_new_file_if_empty(True) + + def save_open_files(self): + """Save the list of open files""" + self.set_option('filenames', self.get_open_filenames()) + + def set_create_new_file_if_empty(self, value): + """Change the value of create_new_file_if_empty""" + for editorstack in self.editorstacks: + editorstack.create_new_file_if_empty = value diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/plugins/explorer.py spyder-3.0.2+dfsg1/spyder/plugins/explorer.py --- spyder-2.3.8+dfsg1/spyder/plugins/explorer.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/plugins/explorer.py 2016-11-19 01:31:58.000000000 +0100 @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- # -# Copyright © 2009-2010 Pierre Raybaut +# Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) +# (see spyder/__init__.py for details) """Files and Directories Explorer Plugin""" @@ -11,22 +11,32 @@ # pylint: disable=R0911 # pylint: disable=R0201 -from spyderlib.qt.QtGui import QFontDialog -from spyderlib.qt.QtCore import SIGNAL - +# Standard library imports import os.path as osp +# Third party imports +from qtpy.QtCore import Signal + # Local imports -from spyderlib.baseconfig import _ -from spyderlib.utils.qthelpers import create_action -from spyderlib.widgets.explorer import ExplorerWidget -from spyderlib.plugins import SpyderPluginMixin -from spyderlib.py3compat import to_text_string +from spyder.config.base import _ +from spyder.plugins import SpyderPluginMixin +from spyder.py3compat import to_text_string +from spyder.widgets.explorer import ExplorerWidget class Explorer(ExplorerWidget, SpyderPluginMixin): """File and Directories Explorer DockWidget""" CONF_SECTION = 'explorer' + open_terminal = Signal(str) + open_interpreter = Signal(str) + edit = Signal(str) + removed = Signal(str) + removed_tree = Signal(str) + renamed = Signal(str, str) + create_module = Signal(str) + run = Signal(str) + open_dir = Signal(str) + def __init__(self, parent=None): ExplorerWidget.__init__(self, parent=parent, name_filters=self.get_option('name_filters'), @@ -36,10 +46,8 @@ # Initialize plugin self.initialize_plugin() - - self.set_font(self.get_plugin_font()) - - #------ SpyderPluginWidget API --------------------------------------------- + + #------ SpyderPluginWidget API --------------------------------------------- def get_plugin_title(self): """Return widget title""" return _("File explorer") @@ -53,41 +61,34 @@ def get_plugin_actions(self): """Return a list of actions related to plugin""" - # Font - font_action = create_action(self, _("&Font..."), None, 'font.png', - _("Set font style"), - triggered=self.change_font) - self.treewidget.common_actions.append(font_action) return [] def register_plugin(self): """Register plugin in Spyder's main window""" self.main.add_dockwidget(self) - self.connect(self, SIGNAL("edit(QString)"), self.main.editor.load) - self.connect(self, SIGNAL("removed(QString)"), self.main.editor.removed) - self.connect(self, SIGNAL("renamed(QString,QString)"), - self.main.editor.renamed) - self.connect(self.main.editor, SIGNAL("open_dir(QString)"), self.chdir) - self.connect(self, SIGNAL("create_module(QString)"), - self.main.editor.new) - self.connect(self, SIGNAL("run(QString)"), + self.edit.connect(self.main.editor.load) + self.removed.connect(self.main.editor.removed) + self.removed_tree.connect(self.main.editor.removed_tree) + self.renamed.connect(self.main.editor.renamed) + self.main.editor.open_dir.connect(self.chdir) + self.create_module.connect(self.main.editor.new) + self.run.connect( lambda fname: self.main.open_external_console(to_text_string(fname), osp.dirname(to_text_string(fname)), '', False, False, True, '', False)) # Signal "set_explorer_cwd(QString)" will refresh only the # contents of path passed by the signal in explorer: - self.connect(self.main.workingdirectory, - SIGNAL("set_explorer_cwd(QString)"), + self.main.workingdirectory.set_explorer_cwd.connect( lambda directory: self.refresh_plugin(new_path=directory, force_current=True)) - self.connect(self, SIGNAL("open_dir(QString)"), + self.open_dir.connect( lambda dirname: self.main.workingdirectory.chdir(dirname, refresh_explorer=False)) self.sig_open_file.connect(self.main.open_file) - self.sig_new_file.connect(self.main.new_file) + self.sig_new_file.connect(lambda t: self.main.editor.new(text=t)) def refresh_plugin(self, new_path=None, force_current=True): """Refresh explorer widget""" @@ -97,21 +98,8 @@ def closing_plugin(self, cancelable=False): """Perform actions before parent main window is closed""" return True - - #------ Public API --------------------------------------------------------- + + #------ Public API --------------------------------------------------------- def chdir(self, directory): """Set working directory""" self.treewidget.chdir(directory) - - def change_font(self): - """Change font""" - font, valid = QFontDialog.getFont(self.get_plugin_font(), self, - _("Select a new font")) - if valid: - self.set_font(font) - self.set_plugin_font(font) - - def set_font(self, font): - """Set explorer widget font""" - self.setFont(font) - self.treewidget.setFont(font) diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/plugins/externalconsole.py spyder-3.0.2+dfsg1/spyder/plugins/externalconsole.py --- spyder-2.3.8+dfsg1/spyder/plugins/externalconsole.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/plugins/externalconsole.py 2016-11-19 01:31:58.000000000 +0100 @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- # -# Copyright © 2009-2010 Pierre Raybaut +# Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) +# (see spyder/__init__.py for details) """External Console plugin""" @@ -11,60 +11,50 @@ # pylint: disable=R0911 # pylint: disable=R0201 -# Qt imports -from spyderlib.qt.QtGui import (QVBoxLayout, QMessageBox, QInputDialog, - QLineEdit, QPushButton, QGroupBox, QLabel, - QTabWidget, QFontComboBox, QHBoxLayout, - QButtonGroup, QWidget) -from spyderlib.qt.QtCore import SIGNAL, Qt -from spyderlib.qt.compat import getopenfilename - -# Stdlib imports -import atexit +# Standard library imports import os import os.path as osp import sys -import subprocess + +# Third party imports +from qtpy import PYQT5 +from qtpy.compat import getopenfilename +from qtpy.QtCore import Qt, Signal, Slot +from qtpy.QtWidgets import (QButtonGroup, QGroupBox, QHBoxLayout, QLabel, + QMessageBox, QTabWidget, QVBoxLayout, QWidget) # Local imports -from spyderlib.baseconfig import SCIENTIFIC_STARTUP, running_in_mac_app, _ -from spyderlib.config import CONF -from spyderlib.utils import encoding, programs -from spyderlib.utils.misc import (get_error_match, get_python_executable, - remove_backslashes, is_python_script) -from spyderlib.utils.qthelpers import get_icon, create_action, mimedata2url -from spyderlib.widgets.tabs import Tabs -from spyderlib.widgets.externalshell.pythonshell import ExternalPythonShell -from spyderlib.widgets.externalshell.systemshell import ExternalSystemShell -from spyderlib.widgets.findreplace import FindReplace -from spyderlib.plugins import SpyderPluginWidget, PluginConfigPage -from spyderlib.plugins.runconfig import get_run_configuration -from spyderlib.py3compat import to_text_string, is_text_string, getcwd -from spyderlib import dependencies +from spyder import dependencies +from spyder.config.base import _, SCIENTIFIC_STARTUP +from spyder.config.main import CONF +from spyder.utils import encoding, programs +from spyder.utils import icon_manager as ima +from spyder.utils.misc import (get_error_match, get_python_executable, + is_python_script, remove_backslashes) +from spyder.utils.qthelpers import create_action, mimedata2url +from spyder.plugins import SpyderPluginWidget +from spyder.plugins.configdialog import PluginConfigPage +from spyder.plugins.runconfig import get_run_configuration +from spyder.py3compat import to_text_string, is_text_string, getcwd +from spyder.widgets.externalshell.pythonshell import ExternalPythonShell +from spyder.widgets.externalshell.systemshell import ExternalSystemShell +from spyder.widgets.findreplace import FindReplace +from spyder.widgets.tabs import Tabs + MPL_REQVER = '>=1.0' dependencies.add("matplotlib", _("Interactive data plotting in the consoles"), - required_version=MPL_REQVER) + required_version=MPL_REQVER, optional=True) class ExternalConsoleConfigPage(PluginConfigPage): + def __init__(self, plugin, parent): PluginConfigPage.__init__(self, plugin, parent) - self.get_name = lambda: _("Console") - self.cus_exec_radio = None - self.pyexec_edit = None - - def initialize(self): - PluginConfigPage.initialize(self) - self.connect(self.pyexec_edit, SIGNAL("textChanged(QString)"), - self.python_executable_changed) - self.connect(self.cus_exec_radio, SIGNAL("toggled(bool)"), - self.python_executable_switched) + self.get_name = lambda: _("Python console") def setup_page(self): interface_group = QGroupBox(_("Interface")) - font_group = self.create_fontgroup(option=None, text=None, - fontfilters=QFontComboBox.MonospacedFonts) newcb = self.create_checkbox singletab_box = newcb(_("One tab per script"), 'single_tab') showtime_box = newcb(_("Show elapsed time"), 'show_elapsed_time') @@ -96,10 +86,8 @@ tip=_("This method is the only way to have colorized standard\n" "error channel when the output channels have been " "merged.")) - self.connect(merge_channels_box, SIGNAL("toggled(bool)"), - colorize_sys_stderr_box.setEnabled) - self.connect(merge_channels_box, SIGNAL("toggled(bool)"), - colorize_sys_stderr_box.setChecked) + merge_channels_box.toggled.connect(colorize_sys_stderr_box.setEnabled) + merge_channels_box.toggled.connect(colorize_sys_stderr_box.setChecked) colorize_sys_stderr_box.setEnabled( self.get_option('merge_output_channels')) @@ -138,77 +126,6 @@ source_layout.addWidget(comp_enter_box) source_layout.addWidget(calltips_box) source_group.setLayout(source_layout) - - # UMR Group - umr_group = QGroupBox(_("User Module Reloader (UMR)")) - umr_label = QLabel(_("UMR forces Python to reload modules which were " - "imported when executing a \nscript in the " - "external console with the 'runfile' function.")) - umr_enabled_box = newcb(_("Enable UMR"), 'umr/enabled', - msg_if_enabled=True, msg_warning=_( - "This option will enable the User Module Reloader (UMR) " - "in Python/IPython consoles. UMR forces Python to " - "reload deeply modules during import when running a " - "Python script using the Spyder's builtin function " - "runfile." - "

    1. UMR may require to restart the " - "console in which it will be called " - "(otherwise only newly imported modules will be " - "reloaded when executing scripts)." - "

    2. If errors occur when re-running a " - "PyQt-based program, please check that the Qt objects " - "are properly destroyed (e.g. you may have to use the " - "attribute Qt.WA_DeleteOnClose on your main " - "window, using the setAttribute method)"), - ) - umr_verbose_box = newcb(_("Show reloaded modules list"), - 'umr/verbose', msg_info=_( - "Please note that these changes will " - "be applied only to new consoles")) - umr_namelist_btn = QPushButton( - _("Set UMR excluded (not reloaded) modules")) - self.connect(umr_namelist_btn, SIGNAL('clicked()'), - self.plugin.set_umr_namelist) - - umr_layout = QVBoxLayout() - umr_layout.addWidget(umr_label) - umr_layout.addWidget(umr_enabled_box) - umr_layout.addWidget(umr_verbose_box) - umr_layout.addWidget(umr_namelist_btn) - umr_group.setLayout(umr_layout) - - # Python executable Group - pyexec_group = QGroupBox(_("Python executable")) - pyexec_bg = QButtonGroup(pyexec_group) - pyexec_label = QLabel(_("Select the Python interpreter executable " - "binary in which Spyder will run scripts:")) - def_exec_radio = self.create_radiobutton( - _("Default (i.e. the same as Spyder's)"), - 'pythonexecutable/default', - button_group=pyexec_bg) - self.cus_exec_radio = self.create_radiobutton( - _("Use the following Python interpreter:"), - 'pythonexecutable/custom', - button_group=pyexec_bg) - if os.name == 'nt': - filters = _("Executables")+" (*.exe)" - else: - filters = None - pyexec_file = self.create_browsefile('', 'pythonexecutable', - filters=filters) - for le in self.lineedits: - if self.lineedits[le][0] == 'pythonexecutable': - self.pyexec_edit = le - self.connect(def_exec_radio, SIGNAL("toggled(bool)"), - pyexec_file.setDisabled) - self.connect(self.cus_exec_radio, SIGNAL("toggled(bool)"), - pyexec_file.setEnabled) - pyexec_layout = QVBoxLayout() - pyexec_layout.addWidget(pyexec_label) - pyexec_layout.addWidget(def_exec_radio) - pyexec_layout.addWidget(self.cus_exec_radio) - pyexec_layout.addWidget(pyexec_file) - pyexec_group.setLayout(pyexec_layout) # PYTHONSTARTUP replacement pystartup_group = QGroupBox(_("PYTHONSTARTUP replacement")) @@ -228,10 +145,8 @@ pystartup_file = self.create_browsefile('', 'pythonstartup', '', filters=_("Python scripts")+\ " (*.py)") - self.connect(def_startup_radio, SIGNAL("toggled(bool)"), - pystartup_file.setDisabled) - self.connect(cus_startup_radio, SIGNAL("toggled(bool)"), - pystartup_file.setEnabled) + def_startup_radio.toggled.connect(pystartup_file.setDisabled) + cus_startup_radio.toggled.connect(pystartup_file.setEnabled) pystartup_layout = QVBoxLayout() pystartup_layout.addWidget(pystartup_label) @@ -252,7 +167,7 @@ monitor_box = newcb(_("Enable monitor"), 'monitor/enabled') for obj in (completion_box, case_comp_box, comp_enter_box, calltips_box): - self.connect(monitor_box, SIGNAL("toggled(bool)"), obj.setEnabled) + monitor_box.toggled.connect(obj.setEnabled) obj.setEnabled(self.get_option('monitor/enabled')) monitor_layout = QVBoxLayout() @@ -261,85 +176,52 @@ monitor_group.setLayout(monitor_layout) # Qt Group - opts = [(_("Default library"), 'default'), ('PyQt4', 'pyqt'), - ('PySide', 'pyside')] - qt_group = QGroupBox(_("Qt (PyQt/PySide)")) + opts = [ + (_("Default library"), 'default'), + ('PyQt5', 'pyqt5'), + ('PyQt4', 'pyqt'), + ('PySide', 'pyside'), + ] + qt_group = QGroupBox(_("Qt-Python Bindings")) qt_setapi_box = self.create_combobox( - _("Qt-Python bindings library selection:"), opts, + _("Library:") + " ", opts, 'qt/api', default='default', tip=_("This option will act on
    " "libraries such as Matplotlib, guidata " "or ETS")) - if self.get_option('pythonexecutable/default'): - interpreter = get_python_executable() - else: - interpreter = self.get_option('pythonexecutable') - has_pyqt4 = programs.is_module_installed('PyQt4', - interpreter=interpreter) - has_pyside = programs.is_module_installed('PySide', - interpreter=interpreter) - if has_pyside and not has_pyqt4: - self.set_option('qt/api', 'pyside') - + qt_layout = QVBoxLayout() qt_layout.addWidget(qt_setapi_box) qt_group.setLayout(qt_layout) - qt_group.setEnabled(has_pyqt4 or has_pyside) - - # PyQt Group - if has_pyqt4: - pyqt_group = QGroupBox(_("PyQt")) - setapi_box = self.create_combobox( - _("API selection for QString and QVariant objects:"), - ((_("Default API"), 0), (_("API #1"), 1), (_("API #2"), 2)), - 'pyqt/api_version', default=0, - tip=_("PyQt API #1 is the default
    " - "API for Python 2. PyQt API #2 is " - "the default API for Python 3 and " - "is compatible with PySide.")) - ignore_api_box = newcb(_("Ignore API change errors (sip.setapi)"), - 'pyqt/ignore_sip_setapi_errors', - tip=_("Enabling this option will ignore
    " - "errors when changing PyQt API. As " - "PyQt does not support dynamic API " - "changes, it is strongly recommended " - "to use this feature wisely, e.g. " - "for debugging purpose.")) - try: - from sip import setapi #analysis:ignore - except ImportError: - setapi_box.setDisabled(True) - ignore_api_box.setDisabled(True) - - pyqt_layout = QVBoxLayout() - pyqt_layout.addWidget(setapi_box) - pyqt_layout.addWidget(ignore_api_box) - pyqt_group.setLayout(pyqt_layout) - qt_layout.addWidget(pyqt_group) - + # Matplotlib Group - mpl_group = QGroupBox(_("Matplotlib")) - mpl_backend_box = newcb('', 'matplotlib/backend/enabled', True) - mpl_backend_edit = self.create_lineedit(_("GUI backend:"), - 'matplotlib/backend/value', "Qt4Agg", - tip=_("Set the GUI toolkit used by
    " - "Matplotlib to show figures " - "(default: Qt4Agg)"), - alignment=Qt.Horizontal) - self.connect(mpl_backend_box, SIGNAL("toggled(bool)"), - mpl_backend_edit.setEnabled) - mpl_backend_layout = QHBoxLayout() - mpl_backend_layout.addWidget(mpl_backend_box) - mpl_backend_layout.addWidget(mpl_backend_edit) - mpl_backend_edit.setEnabled( - self.get_option('matplotlib/backend/enabled')) + mpl_group = QGroupBox(_("Graphics")) + mpl_label = QLabel(_("Decide which backend to use to display graphics. " + "If unsure, please select the Automatic " + "backend.

    " + "Note: We support a very limited number " + "of backends in our Python consoles. If you " + "prefer to work with a different one, please use " + "an IPython console.")) + mpl_label.setWordWrap(True) + + backends = [(_("Automatic"), 0), (_("None"), 1)] + if not os.name == 'nt' and programs.is_module_installed('_tkinter'): + backends.append( ("Tkinter", 2) ) + backends = tuple(backends) + + mpl_backend_box = self.create_combobox( _("Backend:")+" ", backends, + 'matplotlib/backend/value', + tip=_("This option will be applied the " + "next time a console is opened.")) + mpl_installed = programs.is_module_installed('matplotlib') - mpl_layout = QVBoxLayout() - mpl_layout.addLayout(mpl_backend_layout) + mpl_layout.addWidget(mpl_label) + mpl_layout.addWidget(mpl_backend_box) mpl_group.setLayout(mpl_layout) mpl_group.setEnabled(mpl_installed) - + # ETS Group ets_group = QGroupBox(_("Enthought Tool Suite")) ets_label = QLabel(_("Enthought Tool Suite (ETS) supports " @@ -348,23 +230,27 @@ ets_label.setWordWrap(True) ets_edit = self.create_lineedit(_("ETS_TOOLKIT:"), 'ets_backend', alignment=Qt.Horizontal) - + ets_layout = QVBoxLayout() ets_layout.addWidget(ets_label) ets_layout.addWidget(ets_edit) ets_group.setLayout(ets_layout) + + if CONF.get('main_interpreter','default'): + interpreter = get_python_executable() + else: + interpreter = CONF.get('main_interpreter', 'executable') ets_group.setEnabled(programs.is_module_installed( "enthought.etsconfig.api", interpreter=interpreter)) - + tabs = QTabWidget() - tabs.addTab(self.create_tab(font_group, interface_group, display_group, + tabs.addTab(self.create_tab(interface_group, display_group, bg_group), _("Display")) tabs.addTab(self.create_tab(monitor_group, source_group), _("Introspection")) - tabs.addTab(self.create_tab(pyexec_group, pystartup_group, umr_group), - _("Advanced settings")) + tabs.addTab(self.create_tab(pystartup_group), _("Advanced settings")) tabs.addTab(self.create_tab(qt_group, mpl_group, ets_group), _("External modules")) @@ -372,66 +258,6 @@ vlayout.addWidget(tabs) self.setLayout(vlayout) - def _auto_change_qt_api(self, pyexec): - """Change automatically Qt API depending on - selected Python executable""" - has_pyqt4 = programs.is_module_installed('PyQt4', interpreter=pyexec) - has_pyside = programs.is_module_installed('PySide', interpreter=pyexec) - for cb in self.comboboxes: - if self.comboboxes[cb][0] == 'qt/api': - qt_setapi_cb = cb - if has_pyside and not has_pyqt4: - qt_setapi_cb.setCurrentIndex(2) - elif has_pyqt4 and not has_pyside: - qt_setapi_cb.setCurrentIndex(1) - else: - qt_setapi_cb.setCurrentIndex(0) - - def python_executable_changed(self, pyexec): - """Custom Python executable value has been changed""" - if not self.cus_exec_radio.isChecked(): - return - if not is_text_string(pyexec): - pyexec = to_text_string(pyexec.toUtf8(), 'utf-8') - old_pyexec = self.get_option("pythonexecutable", - get_python_executable()) - if pyexec != old_pyexec: - self._auto_change_qt_api(pyexec) - self.warn_python_compatibility(pyexec) - - def python_executable_switched(self, custom): - """Python executable default/custom radio button has been toggled""" - def_pyexec = get_python_executable() - cust_pyexec = self.pyexec_edit.text() - if not is_text_string(cust_pyexec): - cust_pyexec = to_text_string(cust_pyexec.toUtf8(), 'utf-8') - if def_pyexec != cust_pyexec: - pyexec = cust_pyexec if custom else def_pyexec - self._auto_change_qt_api(pyexec) - if custom: - self.warn_python_compatibility(cust_pyexec) - - def warn_python_compatibility(self, pyexec): - if not osp.isfile(pyexec): - return - spyder_version = sys.version_info[0] - try: - cmd = [pyexec, "-c", "import sys; print(sys.version_info[0])"] - # subprocess.check_output is not present in python2.6 and 3.0 - process = subprocess.Popen(cmd, stdout=subprocess.PIPE) - console_version = int(process.communicate()[0]) - except IOError: - console_version = spyder_version - if spyder_version != console_version: - QMessageBox.warning(self, _('Warning'), - _("You selected a Python %d interpreter for the console " - "but Spyder is running on Python %d!.

    " - "Although this is possible, we recommend you to install and " - "run Spyder directly with your selected interpreter, to avoid " - "seeing false warnings and errors due to the incompatible " - "syntax between these two Python versions." - ) % (console_version, spyder_version), QMessageBox.Ok) - class ExternalConsole(SpyderPluginWidget): """ @@ -440,29 +266,25 @@ CONF_SECTION = 'console' CONFIGWIDGET_CLASS = ExternalConsoleConfigPage DISABLE_ACTIONS_WHEN_HIDDEN = False - def __init__(self, parent, light_mode): - SpyderPluginWidget.__init__(self, parent) - self.light_mode = light_mode + + edit_goto = Signal((str, int, str), (str, int, str, bool)) + focus_changed = Signal() + redirect_stdio = Signal(bool) + + def __init__(self, parent): + if PYQT5: + SpyderPluginWidget.__init__(self, parent, main = parent) + else: + SpyderPluginWidget.__init__(self, parent) self.tabwidget = None self.menu_actions = None - - self.inspector = None # Object inspector plugin + + self.help = None # Help plugin self.historylog = None # History log plugin self.variableexplorer = None # Variable explorer plugin - + self.python_count = 0 self.terminal_count = 0 - - try: - from sip import setapi #analysis:ignore - except ImportError: - self.set_option('pyqt/ignore_sip_setapi_errors', False) - - # Python executable selection (initializing default values as well) - executable = self.get_option('pythonexecutable', - get_python_executable()) - if self.get_option('pythonexecutable/default'): - executable = get_python_executable() # Python startup file selection if not osp.isfile(self.get_option('pythonstartup', '')): @@ -470,20 +292,7 @@ # default/custom settings are mutually exclusive: self.set_option('pythonstartup/custom', not self.get_option('pythonstartup/default')) - - if not osp.isfile(executable): - # This is absolutely necessary, in case the Python interpreter - # executable has been moved since last Spyder execution (following - # a Python distribution upgrade for example) - self.set_option('pythonexecutable', get_python_executable()) - elif executable.endswith('pythonw.exe'): - # That should not be necessary because this case is already taken - # care of by the `get_python_executable` function but, this was - # implemented too late, so we have to fix it here too, in case - # the Python executable has already been set with pythonw.exe: - self.set_option('pythonexecutable', - executable.replace("pythonw.exe", "python.exe")) - + self.shellwidgets = [] self.filenames = [] self.icons = [] @@ -500,12 +309,9 @@ # a crash when the console is detached from the main window # Fixes Issue 561 self.tabwidget.setDocumentMode(True) - self.connect(self.tabwidget, SIGNAL('currentChanged(int)'), - self.refresh_plugin) - self.connect(self.tabwidget, SIGNAL('move_data(int,int)'), - self.move_tab) - self.connect(self.main, SIGNAL("pythonpath_changed()"), - self.set_path) + self.tabwidget.currentChanged.connect(self.refresh_plugin) + self.tabwidget.move_data.connect(self.move_tab) + self.main.sig_pythonpath_changed.connect(self.set_path) self.tabwidget.set_close_function(self.close_console) @@ -522,15 +328,15 @@ # Find/replace widget self.find_widget = FindReplace(self) self.find_widget.hide() - self.register_widget_shortcuts("Editor", self.find_widget) - + self.register_widget_shortcuts(self.find_widget) + layout.addWidget(self.find_widget) - + self.setLayout(layout) - + # Accepting drops self.setAcceptDrops(True) - + def move_tab(self, index_from, index_to): """ Move tab (tabs themselves have already been moved by the tabwidget) @@ -542,66 +348,37 @@ self.filenames.insert(index_to, filename) self.shellwidgets.insert(index_to, shell) self.icons.insert(index_to, icons) - self.emit(SIGNAL('update_plugin_title()')) + self.update_plugin_title.emit() def get_shell_index_from_id(self, shell_id): """Return shellwidget index from id""" for index, shell in enumerate(self.shellwidgets): if id(shell) == shell_id: return index - - def close_console(self, index=None, from_ipyclient=False): + + def close_console(self, index=None): """Close console tab from index or widget (or close current tab)""" # Get tab index if not self.tabwidget.count(): return if index is None: index = self.tabwidget.currentIndex() - - # Detect what widget we are trying to close - for i, s in enumerate(self.shellwidgets): - if index == i: - shellwidget = s - - # If the tab is an IPython kernel, try to detect if it has a client - # connected to it - if shellwidget.is_ipykernel: - ipyclients = self.main.ipyconsole.get_clients() - if ipyclients: - for ic in ipyclients: - if ic.kernel_widget_id == id(shellwidget): - connected_ipyclient = True - break - else: - connected_ipyclient = False - else: - connected_ipyclient = False - + # Closing logic - if not shellwidget.is_ipykernel or from_ipyclient or \ - not connected_ipyclient: - self.tabwidget.widget(index).close() - self.tabwidget.removeTab(index) - self.filenames.pop(index) - self.shellwidgets.pop(index) - self.icons.pop(index) - self.emit(SIGNAL('update_plugin_title()')) - else: - QMessageBox.question(self, _('Trying to kill a kernel?'), - _("You can't close this kernel because it has one or more " - "consoles connected to it.

    " - "You need to close them instead or you can kill the kernel " - "using the second button from right to left."), - QMessageBox.Ok) - - + self.tabwidget.widget(index).close() + self.tabwidget.removeTab(index) + self.filenames.pop(index) + self.shellwidgets.pop(index) + self.icons.pop(index) + self.update_plugin_title.emit() + def set_variableexplorer(self, variableexplorer): """Set variable explorer plugin""" self.variableexplorer = variableexplorer def set_path(self): """Set consoles PYTHONPATH if changed by the user""" - from spyderlib.widgets.externalshell import pythonshell + from spyder.widgets.externalshell import pythonshell for sw in self.shellwidgets: if isinstance(sw, pythonshell.ExternalPythonShell): if sw.is_interpreter and sw.is_running(): @@ -612,7 +389,7 @@ current_index = self.tabwidget.currentIndex() if current_index == -1: return - from spyderlib.widgets.externalshell import pythonshell + from spyder.widgets.externalshell import pythonshell for index in [current_index]+list(range(self.tabwidget.count())): shellwidget = self.tabwidget.widget(index) if isinstance(shellwidget, pythonshell.ExternalPythonShell): @@ -623,22 +400,22 @@ else: self.tabwidget.setCurrentIndex(index) return shellwidget - + def get_current_shell(self): """ - Called by object inspector to retrieve the current shell instance + Called by Help to retrieve the current shell instance """ shellwidget = self.__find_python_shell() return shellwidget.shell - + def get_running_python_shell(self): """ - Called by object inspector to retrieve a running Python shell instance + Called by Help to retrieve a running Python shell instance """ current_index = self.tabwidget.currentIndex() if current_index == -1: return - from spyderlib.widgets.externalshell import pythonshell + from spyder.widgets.externalshell import pythonshell shellwidgets = [self.tabwidget.widget(index) for index in range(self.tabwidget.count())] shellwidgets = [_w for _w in shellwidgets @@ -652,7 +429,8 @@ else: return shellwidgets[0].shell - def run_script_in_current_shell(self, filename, wdir, args, debug): + def run_script_in_current_shell(self, filename, wdir, args, debug, + post_mortem): """Run script in current shell, if any""" norm = lambda text: remove_backslashes(to_text_string(text)) line = "%s('%s'" % ('debugfile' if debug else 'runfile', @@ -661,8 +439,10 @@ line += ", args='%s'" % norm(args) if wdir: line += ", wdir='%s'" % norm(wdir) + if post_mortem: + line += ', post_mortem=True' line += ")" - if not self.execute_python_code(line, interpreter_only=True): + if not self.execute_code(line, interpreter_only=True): QMessageBox.warning(self, _('Warning'), _("No Python console is currently selected to run %s." "

    Please select or open a new Python console " @@ -679,45 +459,37 @@ directory = encoding.to_unicode_from_fs(directory) shellwidget.shell.set_cwd(directory) - def execute_python_code(self, lines, interpreter_only=False): - """Execute Python code in an already opened Python interpreter""" + def execute_code(self, lines, interpreter_only=False): + """Execute code in an already opened Python interpreter""" shellwidget = self.__find_python_shell( interpreter_only=interpreter_only) - if (shellwidget is not None) and (not shellwidget.is_ipykernel): + if shellwidget is not None: shellwidget.shell.execute_lines(to_text_string(lines)) self.activateWindow() shellwidget.shell.setFocus() return True else: return False - + def pdb_has_stopped(self, fname, lineno, shellwidget): - """Python debugger has just stopped at frame (fname, lineno)""" - # This is a unique form of the edit_goto signal that is intended to + """Python debugger has just stopped at frame (fname, lineno)""" + # This is a unique form of the edit_goto signal that is intended to # prevent keyboard input from accidentally entering the editor - # during repeated, rapid entry of debugging commands. - self.emit(SIGNAL("edit_goto(QString,int,QString,bool)"), - fname, lineno, '', False) - if shellwidget.is_ipykernel: - # Focus client widget, not kernel - ipw = self.main.ipyconsole.get_focus_widget() - self.main.ipyconsole.activateWindow() - ipw.setFocus() - else: - self.activateWindow() - shellwidget.shell.setFocus() - + # during repeated, rapid entry of debugging commands. + self.edit_goto[str, int, str, bool].emit(fname, lineno, '', False) + self.activateWindow() + shellwidget.shell.setFocus() + def set_spyder_breakpoints(self): """Set all Spyder breakpoints into all shells""" for shellwidget in self.shellwidgets: - shellwidget.shell.set_spyder_breakpoints() - + shellwidget.shell.set_spyder_breakpoints() + def start(self, fname, wdir=None, args='', interact=False, debug=False, - python=True, ipykernel=False, ipyclient=None, - give_ipyclient_focus=True, python_args=''): + python=True, python_args='', post_mortem=True): """ Start new console - + fname: string: filename of script to run None: open an interpreter @@ -726,8 +498,6 @@ interact: inspect script interactively after its execution debug: run pdb python: True: Python interpreter, False: terminal - ipykernel: True: IPython kernel - ipyclient: True: Automatically create an IPython client python_args: additionnal Python interpreter command line options (option "-u" is mandatory, see widgets.externalshell package) """ @@ -766,62 +536,43 @@ light_background = self.get_option('light_background') show_elapsed_time = self.get_option('show_elapsed_time') if python: - if self.get_option('pythonexecutable/default'): + if CONF.get('main_interpreter', 'default'): pythonexecutable = get_python_executable() + external_interpreter = False else: - pythonexecutable = self.get_option('pythonexecutable') - if self.get_option('pythonstartup/default') or ipykernel: + pythonexecutable = CONF.get('main_interpreter', 'executable') + external_interpreter = True + if self.get_option('pythonstartup/default'): pythonstartup = None else: pythonstartup = self.get_option('pythonstartup', None) monitor_enabled = self.get_option('monitor/enabled') - if self.get_option('matplotlib/backend/enabled'): - mpl_backend = self.get_option('matplotlib/backend/value') - else: - mpl_backend = None + mpl_backend = self.get_option('matplotlib/backend/value') ets_backend = self.get_option('ets_backend') qt_api = self.get_option('qt/api') - if qt_api not in ('pyqt', 'pyside'): + if qt_api not in ('pyqt', 'pyside', 'pyqt5'): qt_api = None - pyqt_api = self.get_option('pyqt/api_version') - ignore_sip_setapi_errors = self.get_option( - 'pyqt/ignore_sip_setapi_errors') merge_output_channels = self.get_option('merge_output_channels') colorize_sys_stderr = self.get_option('colorize_sys_stderr') - umr_enabled = self.get_option('umr/enabled') - umr_namelist = self.get_option('umr/namelist') - umr_verbose = self.get_option('umr/verbose') + umr_enabled = CONF.get('main_interpreter', 'umr/enabled') + umr_namelist = CONF.get('main_interpreter', 'umr/namelist') + umr_verbose = CONF.get('main_interpreter', 'umr/verbose') ar_timeout = CONF.get('variable_explorer', 'autorefresh/timeout') ar_state = CONF.get('variable_explorer', 'autorefresh') - # CRUCIAL NOTE FOR IPYTHON KERNELS: - # autorefresh needs to be on so that our monitor - # can find __ipythonkernel__ in the globals namespace - # *after* the kernel has been started. - # Without the ns refresh provided by autorefresh, a - # client is *never* started (although the kernel is) - # Fix Issue 1595 - if not ar_state and ipykernel: - ar_state = True - - if self.light_mode: - from spyderlib.plugins.variableexplorer import VariableExplorer - sa_settings = VariableExplorer.get_settings() - else: - sa_settings = None + sa_settings = None shellwidget = ExternalPythonShell(self, fname, wdir, - interact, debug, path=pythonpath, + interact, debug, post_mortem=post_mortem, + path=pythonpath, python_args=python_args, - ipykernel=ipykernel, arguments=args, stand_alone=sa_settings, pythonstartup=pythonstartup, pythonexecutable=pythonexecutable, + external_interpreter=external_interpreter, umr_enabled=umr_enabled, umr_namelist=umr_namelist, umr_verbose=umr_verbose, ets_backend=ets_backend, monitor_enabled=monitor_enabled, - mpl_backend=mpl_backend, - qt_api=qt_api, pyqt_api=pyqt_api, - ignore_sip_setapi_errors=ignore_sip_setapi_errors, + mpl_backend=mpl_backend, qt_api=qt_api, merge_output_channels=merge_output_channels, colorize_sys_stderr=colorize_sys_stderr, autorefresh_timeout=ar_timeout, @@ -830,10 +581,10 @@ menu_actions=self.menu_actions, show_buttons_inside=False, show_elapsed_time=show_elapsed_time) - self.connect(shellwidget, SIGNAL('pdb(QString,int)'), - lambda fname, lineno, shellwidget=shellwidget: - self.pdb_has_stopped(fname, lineno, shellwidget)) - self.register_widget_shortcuts("Console", shellwidget.shell) + shellwidget.sig_pdb.connect( + lambda fname, lineno, shellwidget=shellwidget: + self.pdb_has_stopped(fname, lineno, shellwidget)) + self.register_widget_shortcuts(shellwidget.shell) else: if os.name == 'posix': cmd = 'gnome-terminal' @@ -867,86 +618,29 @@ self.get_option('codecompletion/case_sensitive') ) shellwidget.shell.set_codecompletion_enter( self.get_option('codecompletion/enter_key') ) - if python and self.inspector is not None: - shellwidget.shell.set_inspector(self.inspector) - shellwidget.shell.set_inspector_enabled( - CONF.get('inspector', 'connect/python_console')) + if python and self.help is not None: + shellwidget.shell.set_help(self.help) + shellwidget.shell.set_help_enabled( + CONF.get('help', 'connect/python_console')) if self.historylog is not None: self.historylog.add_history(shellwidget.shell.history_filename) - self.connect(shellwidget.shell, - SIGNAL('append_to_history(QString,QString)'), - self.historylog.append_to_history) - self.connect(shellwidget.shell, SIGNAL("go_to_error(QString)"), - self.go_to_error) - self.connect(shellwidget.shell, SIGNAL("focus_changed()"), - lambda: self.emit(SIGNAL("focus_changed()"))) + shellwidget.shell.append_to_history.connect( + self.historylog.append_to_history) + shellwidget.shell.go_to_error.connect(self.go_to_error) + shellwidget.shell.focus_changed.connect( + lambda: self.focus_changed.emit()) if python: if self.main.editor is not None: - self.connect(shellwidget, SIGNAL('open_file(QString,int)'), - self.open_file_in_spyder) + shellwidget.open_file.connect(self.open_file_in_spyder) if fname is None: - if ipykernel: - # Connect client to any possible error while starting the - # kernel - ipyclient.connect(shellwidget, - SIGNAL("ipython_kernel_start_error(QString)"), - lambda error: ipyclient.show_kernel_error(error)) - - # Detect if kernel and frontend match or not - # Don't apply this for our Mac app because it's - # failing, see Issue 2006 - if self.get_option('pythonexecutable/custom') and \ - not running_in_mac_app(): - frontend_ver = programs.get_module_version('IPython') - old_vers = ['1', '2'] - if any([frontend_ver.startswith(v) for v in old_vers]): - frontend_ver = '<3.0' - else: - frontend_ver = '>=3.0' - pyexec = self.get_option('pythonexecutable') - kernel_and_frontend_match = \ - programs.is_module_installed('IPython', - version=frontend_ver, - interpreter=pyexec) - else: - kernel_and_frontend_match = True - - # Create a a kernel tab only if frontend and kernel - # versions match - if kernel_and_frontend_match: - tab_name = _("Kernel") - tab_icon1 = get_icon('ipython_console.png') - tab_icon2 = get_icon('ipython_console_t.png') - self.connect(shellwidget, - SIGNAL('create_ipython_client(QString)'), - lambda cf: self.register_ipyclient(cf, - ipyclient, - shellwidget, - give_focus=give_ipyclient_focus)) - else: - shellwidget.emit( - SIGNAL("ipython_kernel_start_error(QString)"), - _("Either:" - "
      " - "
    1. Your IPython frontend and kernel versions " - "are incompatible or
    2. " - "
    3. You don't have IPython installed in " - "your external interpreter.
    4. " - "
    " - "In any case, we're sorry but we can't create a " - "console for you.")) - shellwidget.deleteLater() - shellwidget = None - return - else: - self.python_count += 1 - tab_name = "Python %d" % self.python_count - tab_icon1 = get_icon('python.png') - tab_icon2 = get_icon('python_t.png') + self.python_count += 1 + tab_name = "Python %d" % self.python_count + tab_icon1 = ima.icon('python') + tab_icon2 = ima.icon('python_t') else: tab_name = osp.basename(fname) - tab_icon1 = get_icon('run.png') - tab_icon2 = get_icon('terminated.png') + tab_icon1 = ima.icon('run') + tab_icon2 = ima.icon('terminated') else: fname = id(shellwidget) if os.name == 'nt': @@ -955,8 +649,8 @@ tab_name = _("Terminal") self.terminal_count += 1 tab_name += (" %d" % self.terminal_count) - tab_icon1 = get_icon('cmdprompt.png') - tab_icon2 = get_icon('cmdprompt_t.png') + tab_icon1 = ima.icon('cmdprompt') + tab_icon2 = ima.icon('cmdprompt_t') self.shellwidgets.insert(index, shellwidget) self.filenames.insert(index, fname) self.icons.insert(index, (tab_icon1, tab_icon2)) @@ -965,73 +659,22 @@ else: self.tabwidget.insertTab(index, shellwidget, tab_name) - self.connect(shellwidget, SIGNAL("started()"), + shellwidget.started.connect( lambda sid=id(shellwidget): self.process_started(sid)) - self.connect(shellwidget, SIGNAL("finished()"), + shellwidget.sig_finished.connect( lambda sid=id(shellwidget): self.process_finished(sid)) self.find_widget.set_editor(shellwidget.shell) self.tabwidget.setTabToolTip(index, fname if wdir is None else wdir) self.tabwidget.setCurrentIndex(index) - if self.dockwidget and not self.ismaximized and not ipykernel: + if self.dockwidget and not self.ismaximized: self.dockwidget.setVisible(True) self.dockwidget.raise_() - + shellwidget.set_icontext_visible(self.get_option('show_icontext')) - + # Start process and give focus to console shellwidget.start_shell() - if not ipykernel: - self.activateWindow() - shellwidget.shell.setFocus() - - def set_ipykernel_attrs(self, connection_file, kernel_widget, name): - """Add the pid of the kernel process to an IPython kernel tab""" - # Set connection file - kernel_widget.connection_file = connection_file - - # If we've reached this point then it's safe to assume IPython - # is available, and this import should be valid. - from IPython.core.application import get_ipython_dir - # For each kernel we launch, setup to delete the associated - # connection file at the time Spyder exits. - def cleanup_connection_file(connection_file): - connection_file = osp.join(get_ipython_dir(), 'profile_default', - 'security', connection_file) - try: - os.remove(connection_file) - except OSError: - pass - atexit.register(cleanup_connection_file, connection_file) - - # Set tab name according to client master name - index = self.get_shell_index_from_id(id(kernel_widget)) - tab_name = _("Kernel %s") % name - self.tabwidget.setTabText(index, tab_name) - - def register_ipyclient(self, connection_file, ipyclient, kernel_widget, - give_focus=True): - """ - Register `ipyclient` to be connected to `kernel_widget` - """ - # Check if our client already has a connection_file and kernel_widget_id - # which means that we are asking for a kernel restart - if ipyclient.connection_file is not None \ - and ipyclient.kernel_widget_id is not None: - restart_kernel = True - else: - restart_kernel = False - - # Setting kernel widget attributes - name = ipyclient.name.split('/')[0] - self.set_ipykernel_attrs(connection_file, kernel_widget, name) - - # Creating the client - ipyconsole = self.main.ipyconsole - ipyclient.connection_file = connection_file - ipyclient.kernel_widget_id = id(kernel_widget) - ipyconsole.register_client(ipyclient, restart=restart_kernel, - give_focus=give_focus) - + def open_file_in_spyder(self, fname, lineno): """Open file in Spyder's editor from remote process""" self.main.editor.activateWindow() @@ -1044,11 +687,11 @@ shell = self.shellwidgets[index] icon, _icon = self.icons[index] self.tabwidget.setTabIcon(index, icon) - if self.inspector is not None: - self.inspector.set_shell(shell.shell) + if self.help is not None: + self.help.set_shell(shell.shell) if self.variableexplorer is not None: self.variableexplorer.add_shellwidget(shell) - + def process_finished(self, shell_id): index = self.get_shell_index_from_id(shell_id) if index is not None: @@ -1063,7 +706,7 @@ #------ SpyderPluginWidget API -------------------------------------------- def get_plugin_title(self): """Return widget title""" - title = _('Console') + title = _('Python console') if self.filenames: index = self.tabwidget.currentIndex() fname = self.filenames[index] @@ -1073,7 +716,7 @@ def get_plugin_icon(self): """Return widget icon""" - return get_icon('console.png') + return ima.icon('console') def get_focus_widget(self): """ @@ -1086,7 +729,8 @@ """Return a list of actions related to plugin""" interpreter_action = create_action(self, _("Open a &Python console"), None, - 'python.png', triggered=self.open_interpreter) + ima.icon('python'), + triggered=self.open_interpreter) if os.name == 'nt': text = _("Open &command prompt") tip = _("Open a Windows command prompt") @@ -1097,69 +741,67 @@ triggered=self.open_terminal) run_action = create_action(self, _("&Run..."), None, - 'run_small.png', _("Run a Python script"), + ima.icon('run_small'), _("Run a Python script"), triggered=self.run_script) consoles_menu_actions = [interpreter_action] tools_menu_actions = [terminal_action] self.menu_actions = [interpreter_action, terminal_action, run_action] - + self.main.consoles_menu_actions += consoles_menu_actions self.main.tools_menu_actions += tools_menu_actions - + return self.menu_actions+consoles_menu_actions+tools_menu_actions - + def register_plugin(self): """Register plugin in Spyder's main window""" - if self.main.light: - self.main.setCentralWidget(self) - self.main.widgetlist.append(self) - else: - self.main.add_dockwidget(self) - self.inspector = self.main.inspector - if self.inspector is not None: - self.inspector.set_external_console(self) - self.historylog = self.main.historylog - self.connect(self, SIGNAL("edit_goto(QString,int,QString)"), - self.main.editor.load) - self.connect(self, SIGNAL("edit_goto(QString,int,QString,bool)"), - lambda fname, lineno, word, processevents: - self.main.editor.load(fname, lineno, word, - processevents=processevents)) - self.connect(self.main.editor, - SIGNAL('run_in_current_extconsole(QString,QString,QString,bool)'), - self.run_script_in_current_shell) - self.connect(self.main.editor, - SIGNAL("breakpoints_saved()"), - self.set_spyder_breakpoints) - self.connect(self.main.editor, SIGNAL("open_dir(QString)"), - self.set_current_shell_working_directory) - self.connect(self.main.workingdirectory, - SIGNAL("set_current_console_wd(QString)"), - self.set_current_shell_working_directory) - self.connect(self, SIGNAL('focus_changed()'), - self.main.plugin_focus_changed) - self.connect(self, SIGNAL('redirect_stdio(bool)'), - self.main.redirect_internalshell_stdio) - expl = self.main.explorer - if expl is not None: - self.connect(expl, SIGNAL("open_terminal(QString)"), - self.open_terminal) - self.connect(expl, SIGNAL("open_interpreter(QString)"), - self.open_interpreter) - pexpl = self.main.projectexplorer - if pexpl is not None: - self.connect(pexpl, SIGNAL("open_terminal(QString)"), - self.open_terminal) - self.connect(pexpl, SIGNAL("open_interpreter(QString)"), - self.open_interpreter) - + self.main.add_dockwidget(self) + self.help = self.main.help + self.historylog = self.main.historylog + self.edit_goto.connect(self.main.editor.load) + self.edit_goto[str, int, str, bool].connect( + lambda fname, lineno, word, processevents: + self.main.editor.load(fname, lineno, word, + processevents=processevents)) + self.main.editor.run_in_current_extconsole.connect( + self.run_script_in_current_shell) + self.main.editor.breakpoints_saved.connect( + self.set_spyder_breakpoints) + self.main.editor.open_dir.connect( + self.set_current_shell_working_directory) + self.main.workingdirectory.set_current_console_wd.connect( + self.set_current_shell_working_directory) + self.focus_changed.connect( + self.main.plugin_focus_changed) + self.redirect_stdio.connect( + self.main.redirect_internalshell_stdio) + expl = self.main.explorer + if expl is not None: + expl.open_terminal.connect(self.open_terminal) + expl.open_interpreter.connect(self.open_interpreter) + pexpl = self.main.projects + if pexpl is not None: + pexpl.open_terminal.connect(self.open_terminal) + pexpl.open_interpreter.connect(self.open_interpreter) + def closing_plugin(self, cancelable=False): """Perform actions before parent main window is closed""" for shellwidget in self.shellwidgets: shellwidget.close() return True - + + def restart(self): + """ + Restart the console + + This is needed when we switch project to update PYTHONPATH + and the selected interpreter + """ + self.python_count = 0 + for i in range(len(self.shellwidgets)): + self.close_console() + self.open_interpreter() + def refresh_plugin(self): """Refresh tabwidget""" shellwidget = None @@ -1176,21 +818,30 @@ self.tabwidget.set_corner_widgets({Qt.TopRightCorner: widgets}) if shellwidget: shellwidget.update_time_label_visibility() + self.variableexplorer.set_shellwidget_from_id(id(shellwidget)) + self.help.set_shell(shellwidget.shell) self.main.last_console_plugin_focus_was_python = True - self.emit(SIGNAL('update_plugin_title()')) - + self.update_plugin_title.emit() + + def update_font(self): + """Update font from Preferences""" + font = self.get_plugin_font() + for shellwidget in self.shellwidgets: + shellwidget.shell.set_font(font) + completion_size = CONF.get('main', 'completion/size') + comp_widget = shellwidget.shell.completion_widget + comp_widget.setup_appearance(completion_size, font) + def apply_plugin_settings(self, options): """Apply configuration file's plugin settings""" - font_n = 'plugin_font' - font_o = self.get_plugin_font() showtime_n = 'show_elapsed_time' showtime_o = self.get_option(showtime_n) icontext_n = 'show_icontext' icontext_o = self.get_option(icontext_n) calltips_n = 'calltips' calltips_o = self.get_option(calltips_n) - inspector_n = 'connect_to_oi' - inspector_o = CONF.get('inspector', 'connect/python_console') + help_n = 'connect_to_oi' + help_o = CONF.get('help', 'connect/python_console') wrap_n = 'wrap' wrap_o = self.get_option(wrap_n) compauto_n = 'codecompletion/auto' @@ -1202,21 +853,15 @@ mlc_n = 'max_line_count' mlc_o = self.get_option(mlc_n) for shellwidget in self.shellwidgets: - if font_n in options: - shellwidget.shell.set_font(font_o) - completion_size = CONF.get('shell_appearance', - 'completion/size') - comp_widget = shellwidget.shell.completion_widget - comp_widget.setup_appearance(completion_size, font_o) if showtime_n in options: shellwidget.set_elapsed_time_visible(showtime_o) if icontext_n in options: shellwidget.set_icontext_visible(icontext_o) if calltips_n in options: shellwidget.shell.set_calltips(calltips_o) - if inspector_n in options: + if help_n in options: if isinstance(shellwidget, ExternalPythonShell): - shellwidget.shell.set_inspector_enabled(inspector_o) + shellwidget.shell.set_help_enabled(help_o) if wrap_n in options: shellwidget.shell.toggle_wrap_mode(wrap_o) if compauto_n in options: @@ -1235,94 +880,54 @@ self.dockwidget.show() self.dockwidget.raise_() # Start a console in case there are none shown - from spyderlib.widgets.externalshell import pythonshell + from spyder.widgets.externalshell import pythonshell consoles = None for sw in self.shellwidgets: if isinstance(sw, pythonshell.ExternalPythonShell): - if not sw.is_ipykernel: - consoles = True - break + consoles = True + break if not consoles: self.open_interpreter() else: self.dockwidget.hide() - + #------ Public API --------------------------------------------------------- + @Slot(bool) + @Slot(str) def open_interpreter(self, wdir=None): """Open interpreter""" - if wdir is None: + if not wdir: wdir = getcwd() - if not self.main.light: - self.visibility_changed(True) + self.visibility_changed(True) self.start(fname=None, wdir=to_text_string(wdir), args='', interact=True, debug=False, python=True) - - def start_ipykernel(self, client, wdir=None, give_focus=True): - """Start new IPython kernel""" - if not self.get_option('monitor/enabled'): - QMessageBox.warning(self, _('Open an IPython console'), - _("The console monitor was disabled: the IPython kernel will " - "be started as expected, but an IPython console will have " - "to be connected manually to the kernel."), QMessageBox.Ok) - - if wdir is None: - wdir = getcwd() - self.main.ipyconsole.visibility_changed(True) - self.start(fname=None, wdir=to_text_string(wdir), args='', - interact=True, debug=False, python=True, ipykernel=True, - ipyclient=client, give_ipyclient_focus=give_focus) + @Slot(bool) + @Slot(str) def open_terminal(self, wdir=None): """Open terminal""" - if wdir is None: + if not wdir: wdir = getcwd() self.start(fname=None, wdir=to_text_string(wdir), args='', interact=True, debug=False, python=False) - + + @Slot() def run_script(self): """Run a Python script""" - self.emit(SIGNAL('redirect_stdio(bool)'), False) + self.redirect_stdio.emit(False) filename, _selfilter = getopenfilename(self, _("Run Python script"), getcwd(), _("Python scripts")+" (*.py ; *.pyw ; *.ipy)") - self.emit(SIGNAL('redirect_stdio(bool)'), True) + self.redirect_stdio.emit(True) if filename: self.start(fname=filename, wdir=None, args='', interact=False, debug=False) - - def set_umr_namelist(self): - """Set UMR excluded modules name list""" - arguments, valid = QInputDialog.getText(self, _('UMR'), - _('UMR excluded modules:\n' - '(example: guidata, guiqwt)'), - QLineEdit.Normal, - ", ".join(self.get_option('umr/namelist'))) - if valid: - arguments = to_text_string(arguments) - if arguments: - namelist = arguments.replace(' ', '').split(',') - fixed_namelist = [module_name for module_name in namelist - if programs.is_module_installed(module_name)] - invalid = ", ".join(set(namelist)-set(fixed_namelist)) - if invalid: - QMessageBox.warning(self, _('UMR'), - _("The following modules are not " - "installed on your machine:\n%s" - ) % invalid, QMessageBox.Ok) - QMessageBox.information(self, _('UMR'), - _("Please note that these changes will " - "be applied only to new Python/IPython " - "consoles"), QMessageBox.Ok) - else: - fixed_namelist = [] - self.set_option('umr/namelist', fixed_namelist) - + def go_to_error(self, text): """Go to error if relevant""" match = get_error_match(to_text_string(text)) if match: fname, lnb = match.groups() - self.emit(SIGNAL("edit_goto(QString,int,QString)"), - osp.abspath(fname), int(lnb), '') + self.edit_goto.emit(osp.abspath(fname), int(lnb), '') #----Drag and drop def dragEnterEvent(self, event): diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/plugins/findinfiles.py spyder-3.0.2+dfsg1/spyder/plugins/findinfiles.py --- spyder-2.3.8+dfsg1/spyder/plugins/findinfiles.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/plugins/findinfiles.py 2016-11-19 01:31:58.000000000 +0100 @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- # -# Copyright © 2009-2010 Pierre Raybaut +# Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) +# (see spyder/__init__.py for details) """Find in Files Plugin""" @@ -11,21 +11,31 @@ # pylint: disable=R0911 # pylint: disable=R0201 -from spyderlib.qt.QtGui import QApplication -from spyderlib.qt.QtCore import SIGNAL, Signal +# Standard library imports +import sys + +# Third party imports +from qtpy.QtWidgets import QApplication +from qtpy.QtCore import Signal, Slot # Local imports -from spyderlib.baseconfig import _ -from spyderlib.utils.qthelpers import create_action -from spyderlib.widgets.findinfiles import FindInFilesWidget -from spyderlib.plugins import SpyderPluginMixin -from spyderlib.py3compat import getcwd +from spyder.config.base import _ +from spyder.config.utils import get_edit_extensions +from spyder.py3compat import getcwd +from spyder.utils import icon_manager as ima +from spyder.utils.qthelpers import create_action +from spyder.widgets.findinfiles import FindInFilesWidget +from spyder.plugins import SpyderPluginMixin class FindInFiles(FindInFilesWidget, SpyderPluginMixin): """Find in files DockWidget""" CONF_SECTION = 'find_in_files' sig_option_changed = Signal(str, object) + toggle_visibility = Signal(bool) + edit_goto = Signal(str, int, str) + redirect_stdio = Signal(bool) + def __init__(self, parent=None): supported_encodings = self.get_option('supported_encodings') @@ -35,9 +45,11 @@ search_text = [txt for txt in search_text \ if txt not in self.search_text_samples] search_text += self.search_text_samples - + search_text_regexp = self.get_option('search_text_regexp') include = self.get_option('include') + if not include: + include = self.include_patterns() include_idx = self.get_option('include_idx', None) include_regexp = self.get_option('include_regexp') exclude = self.get_option('exclude') @@ -56,7 +68,7 @@ # Initialize plugin self.initialize_plugin() - self.connect(self, SIGNAL('toggle_visibility(bool)'), self.toggle) + self.toggle_visibility.connect(self.toggle) def toggle(self, state): """Toggle widget visibility""" @@ -67,6 +79,7 @@ """Refresh search directory""" self.find_options.set_directory(getcwd()) + @Slot() def findinfiles_callback(self): """Find in files callback""" widget = QApplication.focusWidget() @@ -83,8 +96,32 @@ self.set_search_text(text) if text: self.find() - - #------ SpyderPluginWidget API --------------------------------------------- + + @staticmethod + def include_patterns(): + """Generate regex common usage patterns to include section.""" + # Change special characters, like + and . to convert into valid re + clean_exts = [] + for ext in get_edit_extensions(): + ext = ext.replace('.', r'\.') + ext = ext.replace('+', r'\+') + clean_exts.append(ext) + + patterns = [r'|'.join([ext + r'$' for ext in clean_exts if ext]) + + r'|README|INSTALL', + r'\.ipy$|\.pyw?$|\.rst$|\.txt$', + '.', + ] + return patterns + + #------ SpyderPluginMixin API --------------------------------------------- + def switch_to_plugin(self): + """Switch to plugin + This method is called when pressing plugin's shortcut key""" + self.findinfiles_callback() # Necessary at least with PyQt5 on Windows + SpyderPluginMixin.switch_to_plugin(self) + + #------ SpyderPluginWidget API -------------------------------------------- def get_plugin_title(self): """Return widget title""" return _("Find in files") @@ -104,15 +141,12 @@ """Register plugin in Spyder's main window""" self.get_pythonpath_callback = self.main.get_spyder_pythonpath self.main.add_dockwidget(self) - self.connect(self, SIGNAL("edit_goto(QString,int,QString)"), - self.main.editor.load) - self.connect(self, SIGNAL('redirect_stdio(bool)'), - self.main.redirect_internalshell_stdio) - self.connect(self.main.workingdirectory, - SIGNAL("refresh_findinfiles()"), self.refreshdir) + self.edit_goto.connect(self.main.editor.load) + self.redirect_stdio.connect(self.main.redirect_internalshell_stdio) + self.main.workingdirectory.refresh_findinfiles.connect(self.refreshdir) findinfiles_action = create_action(self, _("&Find in files"), - icon='findf.png', + icon=ima.icon('findf'), triggered=self.findinfiles_callback, tip=_("Search text in multiple files")) @@ -149,3 +183,15 @@ self.set_option('in_python_path', in_python_path) self.set_option('more_options', more_options) return True + + +def test(): + from spyder.utils.qthelpers import qapplication + app = qapplication() + widget = FindInFiles() + widget.show() + sys.exit(app.exec_()) + + +if __name__ == '__main__': + test() diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/plugins/help.py spyder-3.0.2+dfsg1/spyder/plugins/help.py --- spyder-2.3.8+dfsg1/spyder/plugins/help.py 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/plugins/help.py 2016-11-19 01:31:58.000000000 +0100 @@ -0,0 +1,980 @@ +# -*- coding: utf-8 -*- +# +# Copyright © Spyder Project Contributors +# Licensed under the terms of the MIT License +# (see spyder/__init__.py for details) + +"""Help Plugin""" + +# Standard library imports +import re +import os.path as osp +import socket +import sys + +# Third party imports +from qtpy import PYQT5 +from qtpy.QtCore import QThread, QUrl, Signal, Slot +from qtpy.QtWidgets import (QActionGroup, QComboBox, QGroupBox, QHBoxLayout, + QLabel, QLineEdit, QMenu, QMessageBox, QSizePolicy, + QToolButton, QVBoxLayout, QWidget) +from qtpy.QtWebEngineWidgets import QWebEnginePage, WEBENGINE + +# Local imports +from spyder import dependencies +from spyder.config.base import _, get_conf_path, get_module_source_path +from spyder.config.fonts import DEFAULT_SMALL_DELTA +from spyder.config.ipython import QTCONSOLE_INSTALLED +from spyder.plugins import SpyderPluginWidget +from spyder.plugins.configdialog import PluginConfigPage +from spyder.py3compat import get_meth_class_inst, to_text_string +from spyder.utils import icon_manager as ima +from spyder.utils import programs +from spyder.utils.help.sphinxify import (CSS_PATH, generate_context, + sphinxify, usage, warning) +from spyder.utils.qthelpers import (add_actions, create_action, + create_toolbutton) +from spyder.widgets.browser import FrameWebView +from spyder.widgets.comboboxes import EditableComboBox +from spyder.widgets.findreplace import FindReplace +from spyder.widgets.sourcecode import codeeditor + + +# Sphinx dependency +dependencies.add("sphinx", _("Show help for objects in the Editor and " + "Consoles in a dedicated pane"), + required_version='>=0.6.6') + + + +class ObjectComboBox(EditableComboBox): + """ + QComboBox handling object names + """ + # Signals + valid = Signal(bool, bool) + + def __init__(self, parent): + EditableComboBox.__init__(self, parent) + self.help = parent + self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + self.tips = {True: '', False: ''} + + def is_valid(self, qstr=None): + """Return True if string is valid""" + if not self.help.source_is_console(): + return True + if qstr is None: + qstr = self.currentText() + if not re.search('^[a-zA-Z0-9_\.]*$', str(qstr), 0): + return False + objtxt = to_text_string(qstr) + if self.help.get_option('automatic_import'): + shell = self.help.internal_shell + if shell is not None: + return shell.is_defined(objtxt, force_import=True) + shell = self.help.get_shell() + if shell is not None: + try: + return shell.is_defined(objtxt) + except socket.error: + shell = self.help.get_shell() + try: + return shell.is_defined(objtxt) + except socket.error: + # Well... too bad! + pass + + def validate_current_text(self): + self.validate(self.currentText()) + + def validate(self, qstr, editing=True): + """Reimplemented to avoid formatting actions""" + valid = self.is_valid(qstr) + if self.hasFocus() and valid is not None: + if editing: + # Combo box text is being modified: invalidate the entry + self.show_tip(self.tips[valid]) + self.valid.emit(False, False) + else: + # A new item has just been selected + if valid: + self.selected() + else: + self.valid.emit(False, False) + + +class HelpConfigPage(PluginConfigPage): + def setup_page(self): + # Connections group + connections_group = QGroupBox(_("Automatic connections")) + connections_label = QLabel(_("This pane can automatically " + "show an object's help information after " + "a left parenthesis is written next to it. " + "Below you can decide to which plugin " + "you want to connect it to turn on this " + "feature.")) + connections_label.setWordWrap(True) + editor_box = self.create_checkbox(_("Editor"), 'connect/editor') + rope_installed = programs.is_module_installed('rope') + jedi_installed = programs.is_module_installed('jedi', '>=0.8.1') + editor_box.setEnabled(rope_installed or jedi_installed) + if not rope_installed and not jedi_installed: + editor_tip = _("This feature requires the Rope or Jedi libraries.\n" + "It seems you don't have either installed.") + editor_box.setToolTip(editor_tip) + python_box = self.create_checkbox(_("Python Console"), + 'connect/python_console') + ipython_box = self.create_checkbox(_("IPython Console"), + 'connect/ipython_console') + ipython_box.setEnabled(QTCONSOLE_INSTALLED) + + connections_layout = QVBoxLayout() + connections_layout.addWidget(connections_label) + connections_layout.addWidget(editor_box) + connections_layout.addWidget(python_box) + connections_layout.addWidget(ipython_box) + connections_group.setLayout(connections_layout) + + # Features group + features_group = QGroupBox(_("Additional features")) + math_box = self.create_checkbox(_("Render mathematical equations"), + 'math') + req_sphinx = programs.is_module_installed('sphinx', '>=1.1') + math_box.setEnabled(req_sphinx) + if not req_sphinx: + sphinx_ver = programs.get_module_version('sphinx') + sphinx_tip = _("This feature requires Sphinx 1.1 or superior.") + sphinx_tip += "\n" + _("Sphinx %s is currently installed.") % sphinx_ver + math_box.setToolTip(sphinx_tip) + + features_layout = QVBoxLayout() + features_layout.addWidget(math_box) + features_group.setLayout(features_layout) + + # Source code group + sourcecode_group = QGroupBox(_("Source code")) + wrap_mode_box = self.create_checkbox(_("Wrap lines"), 'wrap') + + sourcecode_layout = QVBoxLayout() + sourcecode_layout.addWidget(wrap_mode_box) + sourcecode_group.setLayout(sourcecode_layout) + + # Final layout + vlayout = QVBoxLayout() + vlayout.addWidget(connections_group) + vlayout.addWidget(features_group) + vlayout.addWidget(sourcecode_group) + vlayout.addStretch(1) + self.setLayout(vlayout) + + +class RichText(QWidget): + """ + WebView widget with find dialog + """ + def __init__(self, parent): + QWidget.__init__(self, parent) + + self.webview = FrameWebView(self) + self.find_widget = FindReplace(self) + self.find_widget.set_editor(self.webview.web_widget) + self.find_widget.hide() + + layout = QVBoxLayout() + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(self.webview) + layout.addWidget(self.find_widget) + self.setLayout(layout) + + def set_font(self, font, fixed_font=None): + """Set font""" + self.webview.set_font(font, fixed_font=fixed_font) + + def set_html(self, html_text, base_url): + """Set html text""" + self.webview.setHtml(html_text, base_url) + + def clear(self): + self.set_html('', self.webview.url()) + + +class PlainText(QWidget): + """ + Read-only editor widget with find dialog + """ + # Signals + focus_changed = Signal() + + def __init__(self, parent): + QWidget.__init__(self, parent) + self.editor = None + + # Read-only editor + self.editor = codeeditor.CodeEditor(self) + self.editor.setup_editor(linenumbers=False, language='py', + scrollflagarea=False, edge_line=False) + self.editor.focus_changed.connect(lambda: self.focus_changed.emit()) + self.editor.setReadOnly(True) + + # Find/replace widget + self.find_widget = FindReplace(self) + self.find_widget.set_editor(self.editor) + self.find_widget.hide() + + layout = QVBoxLayout() + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(self.editor) + layout.addWidget(self.find_widget) + self.setLayout(layout) + + def set_font(self, font, color_scheme=None): + """Set font""" + self.editor.set_font(font, color_scheme=color_scheme) + + def set_color_scheme(self, color_scheme): + """Set color scheme""" + self.editor.set_color_scheme(color_scheme) + + def set_text(self, text, is_code): + self.editor.set_highlight_current_line(is_code) + self.editor.set_occurrence_highlighting(is_code) + if is_code: + self.editor.set_language('py') + else: + self.editor.set_language(None) + self.editor.set_text(text) + self.editor.set_cursor_position('sof') + + def clear(self): + self.editor.clear() + + +class SphinxThread(QThread): + """ + A worker thread for handling rich text rendering. + + Parameters + ---------- + doc : str or dict + A string containing a raw rst text or a dict containing + the doc string components to be rendered. + See spyder.utils.dochelpers.getdoc for description. + context : dict + A dict containing the substitution variables for the + layout template + html_text_no_doc : unicode + Text to be rendered if doc string cannot be extracted. + math_option : bool + Use LaTeX math rendering. + + """ + # Signals + error_msg = Signal(str) + html_ready = Signal(str) + + def __init__(self, html_text_no_doc=''): + super(SphinxThread, self).__init__() + self.doc = None + self.context = None + self.html_text_no_doc = html_text_no_doc + self.math_option = False + + def render(self, doc, context=None, math_option=False, img_path=''): + """Start thread to render a given documentation""" + # If the thread is already running wait for it to finish before + # starting it again. + if self.wait(): + self.doc = doc + self.context = context + self.math_option = math_option + self.img_path = img_path + # This causes run() to be executed in separate thread + self.start() + + def run(self): + html_text = self.html_text_no_doc + doc = self.doc + if doc is not None: + if type(doc) is dict and 'docstring' in doc.keys(): + try: + context = generate_context(name=doc['name'], + argspec=doc['argspec'], + note=doc['note'], + math=self.math_option, + img_path=self.img_path) + html_text = sphinxify(doc['docstring'], context) + if doc['docstring'] == '' and \ + any([doc['name'], doc['argspec'], doc['note']]): + msg = _("No further documentation available") + html_text += '
    ' + html_text += '
    %s
    ' % msg + except Exception as error: + self.error_msg.emit(to_text_string(error)) + return + elif self.context is not None: + try: + html_text = sphinxify(doc, self.context) + except Exception as error: + self.error_msg.emit(to_text_string(error)) + return + self.html_ready.emit(html_text) + + +class Help(SpyderPluginWidget): + """ + Docstrings viewer widget + """ + CONF_SECTION = 'help' + CONFIGWIDGET_CLASS = HelpConfigPage + LOG_PATH = get_conf_path(CONF_SECTION) + FONT_SIZE_DELTA = DEFAULT_SMALL_DELTA + + # Signals + focus_changed = Signal() + + def __init__(self, parent): + if PYQT5: + SpyderPluginWidget.__init__(self, parent, main = parent) + else: + SpyderPluginWidget.__init__(self, parent) + + self.internal_shell = None + + # Initialize plugin + self.initialize_plugin() + + self.no_doc_string = _("No documentation available") + + self._last_console_cb = None + self._last_editor_cb = None + + self.plain_text = PlainText(self) + self.rich_text = RichText(self) + + color_scheme = self.get_color_scheme() + self.set_plain_text_font(self.get_plugin_font(), color_scheme) + self.plain_text.editor.toggle_wrap_mode(self.get_option('wrap')) + + # Add entries to read-only editor context-menu + self.wrap_action = create_action(self, _("Wrap lines"), + toggled=self.toggle_wrap_mode) + self.wrap_action.setChecked(self.get_option('wrap')) + self.plain_text.editor.readonly_menu.addSeparator() + add_actions(self.plain_text.editor.readonly_menu, (self.wrap_action,)) + + self.set_rich_text_font(self.get_plugin_font('rich_text')) + + self.shell = None + + # locked = disable link with Console + self.locked = False + self._last_texts = [None, None] + self._last_editor_doc = None + + # Object name + layout_edit = QHBoxLayout() + layout_edit.setContentsMargins(0, 0, 0, 0) + txt = _("Source") + if sys.platform == 'darwin': + source_label = QLabel(" " + txt) + else: + source_label = QLabel(txt) + layout_edit.addWidget(source_label) + self.source_combo = QComboBox(self) + self.source_combo.addItems([_("Console"), _("Editor")]) + self.source_combo.currentIndexChanged.connect(self.source_changed) + if (not programs.is_module_installed('rope') and + not programs.is_module_installed('jedi', '>=0.8.1')): + self.source_combo.hide() + source_label.hide() + layout_edit.addWidget(self.source_combo) + layout_edit.addSpacing(10) + layout_edit.addWidget(QLabel(_("Object"))) + self.combo = ObjectComboBox(self) + layout_edit.addWidget(self.combo) + self.object_edit = QLineEdit(self) + self.object_edit.setReadOnly(True) + layout_edit.addWidget(self.object_edit) + self.combo.setMaxCount(self.get_option('max_history_entries')) + self.combo.addItems( self.load_history() ) + self.combo.setItemText(0, '') + self.combo.valid.connect(lambda valid: self.force_refresh()) + + # Plain text docstring option + self.docstring = True + self.rich_help = self.get_option('rich_mode', True) + self.plain_text_action = create_action(self, _("Plain Text"), + toggled=self.toggle_plain_text) + + # Source code option + self.show_source_action = create_action(self, _("Show Source"), + toggled=self.toggle_show_source) + + # Rich text option + self.rich_text_action = create_action(self, _("Rich Text"), + toggled=self.toggle_rich_text) + + # Add the help actions to an exclusive QActionGroup + help_actions = QActionGroup(self) + help_actions.setExclusive(True) + help_actions.addAction(self.plain_text_action) + help_actions.addAction(self.rich_text_action) + + # Automatic import option + self.auto_import_action = create_action(self, _("Automatic import"), + toggled=self.toggle_auto_import) + auto_import_state = self.get_option('automatic_import') + self.auto_import_action.setChecked(auto_import_state) + + # Lock checkbox + self.locked_button = create_toolbutton(self, + triggered=self.toggle_locked) + layout_edit.addWidget(self.locked_button) + self._update_lock_icon() + + # Option menu + options_button = create_toolbutton(self, text=_('Options'), + icon=ima.icon('tooloptions')) + options_button.setPopupMode(QToolButton.InstantPopup) + menu = QMenu(self) + add_actions(menu, [self.rich_text_action, self.plain_text_action, + self.show_source_action, None, + self.auto_import_action]) + options_button.setMenu(menu) + layout_edit.addWidget(options_button) + + if self.rich_help: + self.switch_to_rich_text() + else: + self.switch_to_plain_text() + self.plain_text_action.setChecked(not self.rich_help) + self.rich_text_action.setChecked(self.rich_help) + self.source_changed() + + # Main layout + layout = QVBoxLayout() + layout.setContentsMargins(0, 0, 0, 0) + layout.addLayout(layout_edit) + layout.addWidget(self.plain_text) + layout.addWidget(self.rich_text) + self.setLayout(layout) + + # Add worker thread for handling rich text rendering + self._sphinx_thread = SphinxThread( + html_text_no_doc=warning(self.no_doc_string)) + self._sphinx_thread.html_ready.connect( + self._on_sphinx_thread_html_ready) + self._sphinx_thread.error_msg.connect(self._on_sphinx_thread_error_msg) + + # Handle internal and external links + view = self.rich_text.webview + if not WEBENGINE: + view.page().setLinkDelegationPolicy(QWebEnginePage.DelegateAllLinks) + view.linkClicked.connect(self.handle_link_clicks) + + self._starting_up = True + + #------ SpyderPluginWidget API --------------------------------------------- + def on_first_registration(self): + """Action to be performed on first plugin registration""" + self.main.tabify_plugins(self.main.variableexplorer, self) + + def get_plugin_title(self): + """Return widget title""" + return _('Help') + + def get_plugin_icon(self): + """Return widget icon""" + return ima.icon('help') + + def get_focus_widget(self): + """ + Return the widget to give focus to when + this plugin's dockwidget is raised on top-level + """ + self.combo.lineEdit().selectAll() + return self.combo + + def get_plugin_actions(self): + """Return a list of actions related to plugin""" + return [] + + def register_plugin(self): + """Register plugin in Spyder's main window""" + self.focus_changed.connect(self.main.plugin_focus_changed) + self.main.add_dockwidget(self) + self.main.console.set_help(self) + self.internal_shell = self.main.console.shell + + def closing_plugin(self, cancelable=False): + """Perform actions before parent main window is closed""" + return True + + def refresh_plugin(self): + """Refresh widget""" + if self._starting_up: + self._starting_up = False + self.switch_to_rich_text() + self.show_intro_message() + + def update_font(self): + """Update font from Preferences""" + color_scheme = self.get_color_scheme() + font = self.get_plugin_font() + rich_font = self.get_plugin_font(rich_text=True) + + self.set_plain_text_font(font, color_scheme=color_scheme) + self.set_rich_text_font(rich_font) + + def apply_plugin_settings(self, options): + """Apply configuration file's plugin settings""" + color_scheme_n = 'color_scheme_name' + color_scheme_o = self.get_color_scheme() + connect_n = 'connect_to_oi' + wrap_n = 'wrap' + wrap_o = self.get_option(wrap_n) + self.wrap_action.setChecked(wrap_o) + math_n = 'math' + math_o = self.get_option(math_n) + + if color_scheme_n in options: + self.set_plain_text_color_scheme(color_scheme_o) + if wrap_n in options: + self.toggle_wrap_mode(wrap_o) + if math_n in options: + self.toggle_math_mode(math_o) + + # To make auto-connection changes take place instantly + self.main.editor.apply_plugin_settings(options=[connect_n]) + self.main.extconsole.apply_plugin_settings(options=[connect_n]) + if self.main.ipyconsole is not None: + self.main.ipyconsole.apply_plugin_settings(options=[connect_n]) + + #------ Public API (related to Help's source) ------------------------- + def source_is_console(self): + """Return True if source is Console""" + return self.source_combo.currentIndex() == 0 + + def switch_to_editor_source(self): + self.source_combo.setCurrentIndex(1) + + def switch_to_console_source(self): + self.source_combo.setCurrentIndex(0) + + def source_changed(self, index=None): + if self.source_is_console(): + # Console + self.combo.show() + self.object_edit.hide() + self.show_source_action.setEnabled(True) + self.auto_import_action.setEnabled(True) + else: + # Editor + self.combo.hide() + self.object_edit.show() + self.show_source_action.setDisabled(True) + self.auto_import_action.setDisabled(True) + self.restore_text() + + def save_text(self, callback): + if self.source_is_console(): + self._last_console_cb = callback + else: + self._last_editor_cb = callback + + def restore_text(self): + if self.source_is_console(): + cb = self._last_console_cb + else: + cb = self._last_editor_cb + if cb is None: + if self.is_plain_text_mode(): + self.plain_text.clear() + else: + self.rich_text.clear() + else: + func = cb[0] + args = cb[1:] + func(*args) + if get_meth_class_inst(func) is self.rich_text: + self.switch_to_rich_text() + else: + self.switch_to_plain_text() + + #------ Public API (related to rich/plain text widgets) -------------------- + @property + def find_widget(self): + if self.plain_text.isVisible(): + return self.plain_text.find_widget + else: + return self.rich_text.find_widget + + def set_rich_text_font(self, font): + """Set rich text mode font""" + self.rich_text.set_font(font, fixed_font=self.get_plugin_font()) + + def set_plain_text_font(self, font, color_scheme=None): + """Set plain text mode font""" + self.plain_text.set_font(font, color_scheme=color_scheme) + + def set_plain_text_color_scheme(self, color_scheme): + """Set plain text mode color scheme""" + self.plain_text.set_color_scheme(color_scheme) + + @Slot(bool) + def toggle_wrap_mode(self, checked): + """Toggle wrap mode""" + self.plain_text.editor.toggle_wrap_mode(checked) + self.set_option('wrap', checked) + + def toggle_math_mode(self, checked): + """Toggle math mode""" + self.set_option('math', checked) + + def is_plain_text_mode(self): + """Return True if plain text mode is active""" + return self.plain_text.isVisible() + + def is_rich_text_mode(self): + """Return True if rich text mode is active""" + return self.rich_text.isVisible() + + def switch_to_plain_text(self): + """Switch to plain text mode""" + self.rich_help = False + self.plain_text.show() + self.rich_text.hide() + self.plain_text_action.setChecked(True) + + def switch_to_rich_text(self): + """Switch to rich text mode""" + self.rich_help = True + self.plain_text.hide() + self.rich_text.show() + self.rich_text_action.setChecked(True) + self.show_source_action.setChecked(False) + + def set_plain_text(self, text, is_code): + """Set plain text docs""" + + # text is coming from utils.dochelpers.getdoc + if type(text) is dict: + name = text['name'] + if name: + rst_title = ''.join(['='*len(name), '\n', name, '\n', + '='*len(name), '\n\n']) + else: + rst_title = '' + + if text['argspec']: + definition = ''.join(['Definition: ', name, text['argspec'], + '\n']) + else: + definition = '' + + if text['note']: + note = ''.join(['Type: ', text['note'], '\n\n----\n\n']) + else: + note = '' + + full_text = ''.join([rst_title, definition, note, + text['docstring']]) + else: + full_text = text + + self.plain_text.set_text(full_text, is_code) + self.save_text([self.plain_text.set_text, full_text, is_code]) + + def set_rich_text_html(self, html_text, base_url): + """Set rich text""" + self.rich_text.set_html(html_text, base_url) + self.save_text([self.rich_text.set_html, html_text, base_url]) + + def show_intro_message(self): + intro_message = _("Here you can get help of any object by pressing " + "%s in front of it, either on the Editor or the " + "Console.%s" + "Help can also be shown automatically after writing " + "a left parenthesis next to an object. You can " + "activate this behavior in %s.") + prefs = _("Preferences > Help") + if sys.platform == 'darwin': + shortcut = "Cmd+I" + else: + shortcut = "Ctrl+I" + + if self.is_rich_text_mode(): + title = _("Usage") + tutorial_message = _("New to Spyder? Read our") + tutorial = _("tutorial") + intro_message = intro_message % (""+shortcut+"", "

    ", + ""+prefs+"") + self.set_rich_text_html(usage(title, intro_message, + tutorial_message, tutorial), + QUrl.fromLocalFile(CSS_PATH)) + else: + install_sphinx = "\n\n%s" % _("Please consider installing Sphinx " + "to get documentation rendered in " + "rich text.") + intro_message = intro_message % (shortcut, "\n\n", prefs) + intro_message += install_sphinx + self.set_plain_text(intro_message, is_code=False) + + def show_rich_text(self, text, collapse=False, img_path=''): + """Show text in rich mode""" + self.visibility_changed(True) + self.raise_() + self.switch_to_rich_text() + context = generate_context(collapse=collapse, img_path=img_path) + self.render_sphinx_doc(text, context) + + def show_plain_text(self, text): + """Show text in plain mode""" + self.visibility_changed(True) + self.raise_() + self.switch_to_plain_text() + self.set_plain_text(text, is_code=False) + + @Slot() + def show_tutorial(self): + tutorial_path = get_module_source_path('spyder.utils.help') + tutorial = osp.join(tutorial_path, 'tutorial.rst') + text = open(tutorial).read() + self.show_rich_text(text, collapse=True) + + def handle_link_clicks(self, url): + url = to_text_string(url.toString()) + if url == "spy://tutorial": + self.show_tutorial() + elif url.startswith('http'): + programs.start_file(url) + else: + self.rich_text.webview.load(QUrl(url)) + + #------ Public API --------------------------------------------------------- + def force_refresh(self): + if self.source_is_console(): + self.set_object_text(None, force_refresh=True) + elif self._last_editor_doc is not None: + self.set_editor_doc(self._last_editor_doc, force_refresh=True) + + def set_object_text(self, text, force_refresh=False, ignore_unknown=False): + """Set object analyzed by Help""" + if (self.locked and not force_refresh): + return + self.switch_to_console_source() + + add_to_combo = True + if text is None: + text = to_text_string(self.combo.currentText()) + add_to_combo = False + + found = self.show_help(text, ignore_unknown=ignore_unknown) + if ignore_unknown and not found: + return + + if add_to_combo: + self.combo.add_text(text) + if found: + self.save_history() + + if self.dockwidget is not None: + self.dockwidget.blockSignals(True) + self.__eventually_raise_help(text, force=force_refresh) + if self.dockwidget is not None: + self.dockwidget.blockSignals(False) + + def set_editor_doc(self, doc, force_refresh=False): + """ + Use the help plugin to show docstring dictionary computed + with introspection plugin from the Editor plugin + """ + if (self.locked and not force_refresh): + return + self.switch_to_editor_source() + self._last_editor_doc = doc + self.object_edit.setText(doc['obj_text']) + + if self.rich_help: + self.render_sphinx_doc(doc) + else: + self.set_plain_text(doc, is_code=False) + + if self.dockwidget is not None: + self.dockwidget.blockSignals(True) + self.__eventually_raise_help(doc['docstring'], force=force_refresh) + if self.dockwidget is not None: + self.dockwidget.blockSignals(False) + + def __eventually_raise_help(self, text, force=False): + index = self.source_combo.currentIndex() + if hasattr(self.main, 'tabifiedDockWidgets'): + # 'QMainWindow.tabifiedDockWidgets' was introduced in PyQt 4.5 + if self.dockwidget and (force or self.dockwidget.isVisible()) \ + and not self.ismaximized \ + and (force or text != self._last_texts[index]): + dockwidgets = self.main.tabifiedDockWidgets(self.dockwidget) + if self.main.console.dockwidget not in dockwidgets and \ + (hasattr(self.main, 'extconsole') and \ + self.main.extconsole.dockwidget not in dockwidgets): + self.dockwidget.show() + self.dockwidget.raise_() + self._last_texts[index] = text + + def load_history(self, obj=None): + """Load history from a text file in user home directory""" + if osp.isfile(self.LOG_PATH): + history = [line.replace('\n', '') + for line in open(self.LOG_PATH, 'r').readlines()] + else: + history = [] + return history + + def save_history(self): + """Save history to a text file in user home directory""" + open(self.LOG_PATH, 'w').write("\n".join( \ + [to_text_string(self.combo.itemText(index)) + for index in range(self.combo.count())] )) + + @Slot(bool) + def toggle_plain_text(self, checked): + """Toggle plain text docstring""" + if checked: + self.docstring = checked + self.switch_to_plain_text() + self.force_refresh() + self.set_option('rich_mode', not checked) + + @Slot(bool) + def toggle_show_source(self, checked): + """Toggle show source code""" + if checked: + self.switch_to_plain_text() + self.docstring = not checked + self.force_refresh() + self.set_option('rich_mode', not checked) + + @Slot(bool) + def toggle_rich_text(self, checked): + """Toggle between sphinxified docstrings or plain ones""" + if checked: + self.docstring = not checked + self.switch_to_rich_text() + self.set_option('rich_mode', checked) + + @Slot(bool) + def toggle_auto_import(self, checked): + """Toggle automatic import feature""" + self.combo.validate_current_text() + self.set_option('automatic_import', checked) + self.force_refresh() + + @Slot() + def toggle_locked(self): + """ + Toggle locked state + locked = disable link with Console + """ + self.locked = not self.locked + self._update_lock_icon() + + def _update_lock_icon(self): + """Update locked state icon""" + icon = ima.icon('lock') if self.locked else ima.icon('lock_open') + self.locked_button.setIcon(icon) + tip = _("Unlock") if self.locked else _("Lock") + self.locked_button.setToolTip(tip) + + def set_shell(self, shell): + """Bind to shell""" + self.shell = shell + + def get_shell(self): + """ + Return shell which is currently bound to Help, + or another running shell if it has been terminated + """ + if not hasattr(self.shell, 'get_doc') or not self.shell.is_running(): + self.shell = None + if self.main.ipyconsole is not None: + shell = self.main.ipyconsole.get_current_shellwidget() + if shell is not None and shell.kernel_client is not None: + self.shell = shell + if self.shell is None and self.main.extconsole is not None: + self.shell = self.main.extconsole.get_running_python_shell() + if self.shell is None: + self.shell = self.internal_shell + return self.shell + + def render_sphinx_doc(self, doc, context=None): + """Transform doc string dictionary to HTML and show it""" + # Math rendering option could have changed + fname = self.parent().parent().editor.get_current_filename() + dname = osp.dirname(fname) + self._sphinx_thread.render(doc, context, self.get_option('math'), + dname) + + def _on_sphinx_thread_html_ready(self, html_text): + """Set our sphinx documentation based on thread result""" + self._sphinx_thread.wait() + self.set_rich_text_html(html_text, QUrl.fromLocalFile(CSS_PATH)) + + def _on_sphinx_thread_error_msg(self, error_msg): + """ Display error message on Sphinx rich text failure""" + self._sphinx_thread.wait() + self.plain_text_action.setChecked(True) + sphinx_ver = programs.get_module_version('sphinx') + QMessageBox.critical(self, + _('Help'), + _("The following error occured when calling " + "Sphinx %s.
    Incompatible Sphinx " + "version or doc string decoding failed." + "

    Error message:
    %s" + ) % (sphinx_ver, error_msg)) + + def show_help(self, obj_text, ignore_unknown=False): + """Show help""" + shell = self.get_shell() + if shell is None: + return + obj_text = to_text_string(obj_text) + + if not shell.is_defined(obj_text): + if self.get_option('automatic_import') and \ + self.internal_shell.is_defined(obj_text, force_import=True): + shell = self.internal_shell + else: + shell = None + doc = None + source_text = None + + if shell is not None: + doc = shell.get_doc(obj_text) + source_text = shell.get_source(obj_text) + + is_code = False + + if self.rich_help: + self.render_sphinx_doc(doc) + return doc is not None + elif self.docstring: + hlp_text = doc + if hlp_text is None: + hlp_text = source_text + if hlp_text is None: + hlp_text = self.no_doc_string + if ignore_unknown: + return False + else: + hlp_text = source_text + if hlp_text is None: + hlp_text = doc + if hlp_text is None: + hlp_text = _("No source code available.") + if ignore_unknown: + return False + else: + is_code = True + self.set_plain_text(hlp_text, is_code=is_code) + return True diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/plugins/history.py spyder-3.0.2+dfsg1/spyder/plugins/history.py --- spyder-2.3.8+dfsg1/spyder/plugins/history.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/plugins/history.py 2016-11-19 01:31:58.000000000 +0100 @@ -1,36 +1,39 @@ # -*- coding: utf-8 -*- # -# Copyright © 2009-2010 Pierre Raybaut +# Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) +# (see spyder/__init__.py for details) """Console History Plugin""" -from spyderlib.qt.QtGui import (QVBoxLayout, QFontDialog, QInputDialog, - QToolButton, QMenu, QFontComboBox, QGroupBox, - QHBoxLayout, QWidget) -from spyderlib.qt.QtCore import SIGNAL - +# Standard library imports import os.path as osp import sys +# Third party imports +from qtpy import PYQT5 +from qtpy.QtCore import Signal, Slot +from qtpy.QtWidgets import (QGroupBox, QHBoxLayout, QInputDialog, QMenu, + QToolButton, QVBoxLayout, QWidget) + + # Local imports -from spyderlib.utils import encoding -from spyderlib.baseconfig import _ -from spyderlib.config import CONF -from spyderlib.guiconfig import get_color_scheme -from spyderlib.utils.qthelpers import (get_icon, create_action, - create_toolbutton, add_actions) -from spyderlib.widgets.tabs import Tabs -from spyderlib.widgets.sourcecode import codeeditor -from spyderlib.widgets.findreplace import FindReplace -from spyderlib.plugins import SpyderPluginWidget, PluginConfigPage -from spyderlib.py3compat import to_text_string, is_text_string +from spyder.utils import encoding +from spyder.config.base import _ +from spyder.plugins import SpyderPluginWidget +from spyder.plugins.configdialog import PluginConfigPage +from spyder.py3compat import is_text_string, to_text_string +from spyder.utils import icon_manager as ima +from spyder.utils.qthelpers import (add_actions, create_action, + create_toolbutton) +from spyder.widgets.tabs import Tabs +from spyder.widgets.sourcecode import codeeditor +from spyder.widgets.findreplace import FindReplace class HistoryConfigPage(PluginConfigPage): def get_icon(self): - return get_icon('history24.png') + return ima.icon('history') def setup_page(self): settings_group = QGroupBox(_("Settings")) @@ -43,13 +46,6 @@ wrap_mode_box = self.create_checkbox(_("Wrap lines"), 'wrap') go_to_eof_box = self.create_checkbox( _("Scroll automatically to last entry"), 'go_to_eof') - font_group = self.create_fontgroup(option=None, - text=_("Font style"), - fontfilters=QFontComboBox.MonospacedFonts) - names = CONF.get('color_schemes', 'names') - choices = list(zip(names, names)) - cs_combo = self.create_combobox(_("Syntax color scheme: "), - choices, 'color_scheme_name') settings_layout = QVBoxLayout() settings_layout.addWidget(hist_spin) @@ -58,12 +54,10 @@ sourcecode_layout = QVBoxLayout() sourcecode_layout.addWidget(wrap_mode_box) sourcecode_layout.addWidget(go_to_eof_box) - sourcecode_layout.addWidget(cs_combo) sourcecode_group.setLayout(sourcecode_layout) - + vlayout = QVBoxLayout() vlayout.addWidget(settings_group) - vlayout.addWidget(font_group) vlayout.addWidget(sourcecode_group) vlayout.addStretch(1) self.setLayout(vlayout) @@ -75,6 +69,8 @@ """ CONF_SECTION = 'historylog' CONFIGWIDGET_CLASS = HistoryConfigPage + focus_changed = Signal() + def __init__(self, parent): self.tabwidget = None self.menu_actions = None @@ -83,21 +79,18 @@ self.editors = [] self.filenames = [] - self.icons = [] - - SpyderPluginWidget.__init__(self, parent) + if PYQT5: + SpyderPluginWidget.__init__(self, parent, main = parent) + else: + SpyderPluginWidget.__init__(self, parent) # Initialize plugin self.initialize_plugin() - - self.set_default_color_scheme() - + layout = QVBoxLayout() self.tabwidget = Tabs(self, self.menu_actions) - self.connect(self.tabwidget, SIGNAL('currentChanged(int)'), - self.refresh_plugin) - self.connect(self.tabwidget, SIGNAL('move_data(int,int)'), - self.move_tab) + self.tabwidget.currentChanged.connect(self.refresh_plugin) + self.tabwidget.move_data.connect(self.move_tab) if sys.platform == 'darwin': tab_container = QWidget() @@ -110,8 +103,8 @@ layout.addWidget(self.tabwidget) # Menu as corner widget - options_button = create_toolbutton(self, text=_("Options"), - icon=get_icon('tooloptions.png')) + options_button = create_toolbutton(self, text=_('Options'), + icon=ima.icon('tooloptions')) options_button.setPopupMode(QToolButton.InstantPopup) menu = QMenu(self) add_actions(menu, self.menu_actions) @@ -121,20 +114,20 @@ # Find/replace widget self.find_widget = FindReplace(self) self.find_widget.hide() - self.register_widget_shortcuts("Editor", self.find_widget) - + self.register_widget_shortcuts(self.find_widget) + layout.addWidget(self.find_widget) - + self.setLayout(layout) - - #------ SpyderPluginWidget API --------------------------------------------- + + #------ SpyderPluginWidget API --------------------------------------------- def get_plugin_title(self): """Return widget title""" return _('History log') def get_plugin_icon(self): """Return widget icon""" - return get_icon('history.png') + return ima.icon('history') def get_focus_widget(self): """ @@ -158,16 +151,13 @@ def get_plugin_actions(self): """Return a list of actions related to plugin""" history_action = create_action(self, _("History..."), - None, 'history.png', + None, ima.icon('history'), _("Set history maximum entries"), triggered=self.change_history_depth) - font_action = create_action(self, _("&Font..."), None, - 'font.png', _("Set shell font style"), - triggered=self.change_font) self.wrap_action = create_action(self, _("Wrap lines"), toggled=self.toggle_wrap_mode) self.wrap_action.setChecked( self.get_option('wrap') ) - self.menu_actions = [history_action, font_action, self.wrap_action] + self.menu_actions = [history_action, self.wrap_action] return self.menu_actions def on_first_registration(self): @@ -176,17 +166,22 @@ def register_plugin(self): """Register plugin in Spyder's main window""" - self.connect(self, SIGNAL('focus_changed()'), - self.main.plugin_focus_changed) + self.focus_changed.connect(self.main.plugin_focus_changed) self.main.add_dockwidget(self) # self.main.console.set_historylog(self) - self.connect(self.main.console.shell, SIGNAL("refresh()"), - self.refresh_plugin) + self.main.console.shell.refresh.connect(self.refresh_plugin) + + def update_font(self): + """Update font from Preferences""" + color_scheme = self.get_color_scheme() + font = self.get_plugin_font() + for editor in self.editors: + editor.set_font(font, color_scheme) def apply_plugin_settings(self, options): """Apply configuration file's plugin settings""" color_scheme_n = 'color_scheme_name' - color_scheme_o = get_color_scheme(self.get_option(color_scheme_n)) + color_scheme_o = self.get_color_scheme() font_n = 'plugin_font' font_o = self.get_plugin_font() wrap_n = 'wrap' @@ -208,17 +203,15 @@ """ filename = self.filenames.pop(index_from) editor = self.editors.pop(index_from) - icon = self.icons.pop(index_from) self.filenames.insert(index_to, filename) self.editors.insert(index_to, editor) - self.icons.insert(index_to, icon) #------ Public API --------------------------------------------------------- def add_history(self, filename): """ Add new history tab - Slot for SIGNAL('add_history(QString)') emitted by shell instance + Slot for add_history signal emitted by shell instance """ filename = encoding.to_unicode_from_fs(filename) if filename in self.filenames: @@ -226,16 +219,13 @@ editor = codeeditor.CodeEditor(self) if osp.splitext(filename)[1] == '.py': language = 'py' - icon = get_icon('python.png') else: language = 'bat' - icon = get_icon('cmdprompt.png') editor.setup_editor(linenumbers=False, language=language, scrollflagarea=False) - self.connect(editor, SIGNAL("focus_changed()"), - lambda: self.emit(SIGNAL("focus_changed()"))) + editor.focus_changed.connect(lambda: self.focus_changed.emit()) editor.setReadOnly(True) - color_scheme = get_color_scheme(self.get_option('color_scheme_name')) + color_scheme = self.get_color_scheme() editor.set_font( self.get_plugin_font(), color_scheme ) editor.toggle_wrap_mode( self.get_option('wrap') ) @@ -245,18 +235,15 @@ self.editors.append(editor) self.filenames.append(filename) - self.icons.append(icon) index = self.tabwidget.addTab(editor, osp.basename(filename)) self.find_widget.set_editor(editor) self.tabwidget.setTabToolTip(index, filename) - self.tabwidget.setTabIcon(index, icon) self.tabwidget.setCurrentIndex(index) def append_to_history(self, filename, command): """ Append an entry to history filename - Slot for SIGNAL('append_to_history(QString,QString)') - emitted by shell instance + Slot for append_to_history signal emitted by shell instance """ if not is_text_string(filename): # filename is a QString filename = to_text_string(filename.toUtf8(), 'utf-8') @@ -266,25 +253,18 @@ if self.get_option('go_to_eof'): self.editors[index].set_cursor_position('eof') self.tabwidget.setCurrentIndex(index) - + + @Slot() def change_history_depth(self): "Change history max entries""" - depth, valid = QInputDialog.getInteger(self, _('History'), + depth, valid = QInputDialog.getInt(self, _('History'), _('Maximum entries'), self.get_option('max_entries'), 10, 10000) if valid: self.set_option('max_entries', depth) - - def change_font(self): - """Change console font""" - font, valid = QFontDialog.getFont(self.get_plugin_font(), - self, _("Select a new font")) - if valid: - for editor in self.editors: - editor.set_font(font) - self.set_plugin_font(font) - + + @Slot(bool) def toggle_wrap_mode(self, checked): """Toggle wrap mode""" if self.tabwidget is None: diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/plugins/__init__.py spyder-3.0.2+dfsg1/spyder/plugins/__init__.py --- spyder-2.3.8+dfsg1/spyder/plugins/__init__.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/plugins/__init__.py 2016-11-19 01:31:58.000000000 +0100 @@ -1,12 +1,12 @@ # -*- coding: utf-8 -*- # -# Copyright © 2009-2010 Pierre Raybaut +# Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) +# (see spyder/__init__.py for details) """ -spyderlib.plugins -================= +spyder.plugins +============== Here, 'plugins' are widgets designed specifically for Spyder These plugins inherit the following classes @@ -18,49 +18,209 @@ # pylint: disable=R0911 # pylint: disable=R0201 -from spyderlib.qt.QtGui import (QDockWidget, QWidget, QShortcut, QCursor, - QKeySequence, QMainWindow, QApplication) -from spyderlib.qt.QtCore import SIGNAL, Qt, QObject, Signal - -import sys +# Standard library imports +import inspect +import os + +# Third party imports +from qtpy import PYQT5 +from qtpy.QtCore import QEvent, QObject, QPoint, Qt, Signal +from qtpy.QtGui import QCursor, QKeySequence +from qtpy.QtWidgets import (QApplication, QDockWidget, QMainWindow, + QShortcut, QTabBar, QWidget) # Local imports -from spyderlib.utils.qthelpers import toggle_actions, get_icon, create_action -from spyderlib.baseconfig import _ -from spyderlib.config import CONF -from spyderlib.userconfig import NoDefault -from spyderlib.guiconfig import get_font, set_font -from spyderlib.plugins.configdialog import SpyderConfigPage -from spyderlib.py3compat import configparser, is_text_string - - -class PluginConfigPage(SpyderConfigPage): - """Plugin configuration dialog box page widget""" - def __init__(self, plugin, parent): - self.plugin = plugin - self.get_option = plugin.get_option - self.set_option = plugin.set_option - self.get_font = plugin.get_plugin_font - self.set_font = plugin.set_plugin_font - self.apply_settings = plugin.apply_plugin_settings - SpyderConfigPage.__init__(self, parent) - - def get_name(self): - return self.plugin.get_plugin_title() +from spyder.config.base import _ +from spyder.config.gui import get_color_scheme, get_font +from spyder.config.main import CONF +from spyder.config.user import NoDefault +from spyder.py3compat import configparser, is_text_string +from spyder.utils import icon_manager as ima +from spyder.utils.qthelpers import create_action, toggle_actions - def get_icon(self): - return self.plugin.get_plugin_icon() + +class TabFilter(QObject): + """ + Filter event attached to each QTabBar that holds 2 or more dockwidgets in + charge of handling tab rearangement. + + This filter also holds the methods needed for the detection of a drag and + the movement of tabs. + """ + def __init__(self, dock_tabbar, main): + QObject.__init__(self) + self.dock_tabbar = dock_tabbar + self.main = main + self.moving = False + self.from_index = None + self.to_index = None + + # Helper methods + def _get_plugin(self, index): + """Get plugin reference based on tab index.""" + for plugin in self.main.widgetlist: + tab_text = self.dock_tabbar.tabText(index).replace('&', '') + if plugin.get_plugin_title() == tab_text: + return plugin + + def _get_plugins(self): + """ + Get a list of all plugin references in the QTabBar to which this + event filter is attached. + """ + plugins = [] + for index in range(self.dock_tabbar.count()): + plugin = self._get_plugin(index) + plugins.append(plugin) + return plugins + + def _fix_cursor(self, from_index, to_index): + """Fix mouse cursor position to adjust for different tab sizes.""" + # The direction is +1 (moving to the right) or -1 (moving to the left) + direction = abs(to_index - from_index)/(to_index - from_index) + + tab_width = self.dock_tabbar.tabRect(to_index).width() + tab_x_min = self.dock_tabbar.tabRect(to_index).x() + tab_x_max = tab_x_min + tab_width + previous_width = self.dock_tabbar.tabRect(to_index - direction).width() + + delta = previous_width - tab_width + if delta > 0: + delta = delta * direction + else: + delta = 0 + cursor = QCursor() + pos = self.dock_tabbar.mapFromGlobal(cursor.pos()) + x, y = pos.x(), pos.y() + if x < tab_x_min or x > tab_x_max: + new_pos = self.dock_tabbar.mapToGlobal(QPoint(x + delta, y)) + cursor.setPos(new_pos) + + def eventFilter(self, obj, event): + """Filter mouse press events. + + Events that are captured and not propagated return True. Events that + are not captured and are propagated return False. + """ + event_type = event.type() + if event_type == QEvent.MouseButtonPress: + self.tab_pressed(event) + return False + if event_type == QEvent.MouseMove: + self.tab_moved(event) + return True + if event_type == QEvent.MouseButtonRelease: + self.tab_released(event) + return True + return False + + def tab_pressed(self, event): + """Method called when a tab from a QTabBar has been pressed.""" + self.from_index = self.dock_tabbar.tabAt(event.pos()) + self.dock_tabbar.setCurrentIndex(self.from_index) + + if event.button() == Qt.RightButton: + if self.from_index == -1: + self.show_nontab_menu(event) + else: + self.show_tab_menu(event) + + def tab_moved(self, event): + """Method called when a tab from a QTabBar has been moved.""" + # If the left button isn't pressed anymore then return + if not event.buttons() & Qt.LeftButton: + self.to_index = None + return + + self.to_index = self.dock_tabbar.tabAt(event.pos()) + + if not self.moving and self.from_index != -1 and self.to_index != -1: + QApplication.setOverrideCursor(Qt.ClosedHandCursor) + self.moving = True + + if self.to_index == -1: + self.to_index = self.from_index + + from_index, to_index = self.from_index, self.to_index + if from_index != to_index and from_index != -1 and to_index != -1: + self.move_tab(from_index, to_index) + self._fix_cursor(from_index, to_index) + self.from_index = to_index + + def tab_released(self, event): + """Method called when a tab from a QTabBar has been released.""" + QApplication.restoreOverrideCursor() + self.moving = False + + def move_tab(self, from_index, to_index): + """Move a tab from a given index to a given index position.""" + plugins = self._get_plugins() + from_plugin = self._get_plugin(from_index) + to_plugin = self._get_plugin(to_index) + + from_idx = plugins.index(from_plugin) + to_idx = plugins.index(to_plugin) + + plugins[from_idx], plugins[to_idx] = plugins[to_idx], plugins[from_idx] + + for i in range(len(plugins)-1): + self.main.tabify_plugins(plugins[i], plugins[i+1]) + from_plugin.dockwidget.raise_() + + def show_tab_menu(self, event): + """Show the context menu assigned to tabs.""" + self.show_nontab_menu(event) + + def show_nontab_menu(self, event): + """Show the context menu assigned to nontabs section.""" + menu = self.main.createPopupMenu() + menu.exec_(self.dock_tabbar.mapToGlobal(event.pos())) class SpyderDockWidget(QDockWidget): """Subclass to override needed methods""" + plugin_closed = Signal() + + def __init__(self, title, parent): + super(SpyderDockWidget, self).__init__(title, parent) + + # Needed for the installation of the event filter + self.title = title + self.main = parent + self.dock_tabbar = None + + # To track dockwidget changes the filter is installed when dockwidget + # visibility changes. This installs the filter on startup and also + # on dockwidgets that are undocked and then docked to a new location. + self.visibilityChanged.connect(self.install_tab_event_filter) def closeEvent(self, event): """ Reimplement Qt method to send a signal on close so that "Panes" main window menu can be updated correctly """ - self.emit(SIGNAL('plugin_closed()')) + self.plugin_closed.emit() + + def install_tab_event_filter(self, value): + """ + Install an event filter to capture mouse events in the tabs of a + QTabBar holding tabified dockwidgets. + """ + dock_tabbar = None + tabbars = self.main.findChildren(QTabBar) + for tabbar in tabbars: + for tab in range(tabbar.count()): + title = tabbar.tabText(tab) + if title == self.title: + dock_tabbar = tabbar + break + if dock_tabbar is not None: + self.dock_tabbar = dock_tabbar + # Install filter only once per QTabBar + if getattr(self.dock_tabbar, 'filter', None) is None: + self.dock_tabbar.filter = TabFilter(self.dock_tabbar, + self.main) + self.dock_tabbar.installEventFilter(self.dock_tabbar.filter) class SpyderPluginMixin(object): @@ -69,24 +229,32 @@ See SpyderPluginWidget class for required widget interface Signals: - sig_option_changed - Example: - plugin.sig_option_changed.emit('show_all', checked) - 'show_message(QString,int)' + * sig_option_changed + Example: + plugin.sig_option_changed.emit('show_all', checked) + * show_message + * update_plugin_title """ CONF_SECTION = None CONFIGWIDGET_CLASS = None + FONT_SIZE_DELTA = 0 + RICH_FONT_SIZE_DELTA = 0 + IMG_PATH = 'images' ALLOWED_AREAS = Qt.AllDockWidgetAreas LOCATION = Qt.LeftDockWidgetArea - FEATURES = QDockWidget.DockWidgetClosable | \ - QDockWidget.DockWidgetFloatable | \ - QDockWidget.DockWidgetMovable + FEATURES = QDockWidget.DockWidgetClosable | QDockWidget.DockWidgetFloatable DISABLE_ACTIONS_WHEN_HIDDEN = True + + # Signals sig_option_changed = None - def __init__(self, main): + show_message = None + update_plugin_title = None + + def __init__(self, main=None, **kwds): """Bind widget to a QMainWindow instance""" - super(SpyderPluginMixin, self).__init__() + super(SpyderPluginMixin, self).__init__(**kwds) assert self.CONF_SECTION is not None + self.PLUGIN_PATH = os.path.dirname(inspect.getfile(self.__class__)) self.main = main self.default_margins = None self.plugin_actions = None @@ -107,19 +275,19 @@ # We decided to create our own toggle action instead of using # the one that comes with dockwidget because it's not possible # to raise and focus the plugin with it. - self.toggle_view_action = None + self.toggle_view_action = None def initialize_plugin(self): """Initialize plugin: connect signals, setup actions, ...""" + self.create_toggle_view_action() self.plugin_actions = self.get_plugin_actions() - QObject.connect(self, SIGNAL('show_message(QString,int)'), - self.show_message) - QObject.connect(self, SIGNAL('update_plugin_title()'), - self.__update_plugin_title) + if self.show_message is not None: + self.show_message.connect(self.__show_message) + if self.update_plugin_title is not None: + self.update_plugin_title.connect(self.__update_plugin_title) if self.sig_option_changed is not None: self.sig_option_changed.connect(self.set_option) self.setWindowTitle(self.get_plugin_title()) - self.create_toggle_view_action() def on_first_registration(self): """Action to be performed on first plugin registration""" @@ -178,10 +346,8 @@ dock.setFeatures(self.FEATURES) dock.setWidget(self) self.update_margins() - self.connect(dock, SIGNAL('visibilityChanged(bool)'), - self.visibility_changed) - self.connect(dock, SIGNAL('plugin_closed()'), - self.plugin_closed) + dock.visibilityChanged.connect(self.visibility_changed) + dock.plugin_closed.connect(self.plugin_closed) self.dockwidget = dock if self.shortcut is not None: sc = QShortcut(QKeySequence(self.shortcut), self.main, @@ -196,9 +362,9 @@ """ self.mainwindow = mainwindow = QMainWindow() mainwindow.setAttribute(Qt.WA_DeleteOnClose) - icon = self.get_widget_icon() + icon = self.get_plugin_icon() if is_text_string(icon): - icon = get_icon(icon) + icon = self.get_icon(icon) mainwindow.setWindowIcon(icon) mainwindow.setWindowTitle(self.get_plugin_title()) mainwindow.setCentralWidget(self) @@ -215,24 +381,26 @@ def apply_plugin_settings(self, options): """Apply configuration file's plugin settings""" raise NotImplementedError - + def register_shortcut(self, qaction_or_qshortcut, context, name, - default=NoDefault): + add_sc_to_tip=False): """ - Register QAction or QShortcut to Spyder main application, - with shortcut (context, name, default) + Register QAction or QShortcut to Spyder main application + + if add_sc_to_tip is True, the shortcut is added to the + action's tooltip """ - self.main.register_shortcut(qaction_or_qshortcut, - context, name, default) - - def register_widget_shortcuts(self, context, widget): + self.main.register_shortcut(qaction_or_qshortcut, context, + name, add_sc_to_tip) + + def register_widget_shortcuts(self, widget): """ Register widget shortcuts widget interface must have a method called 'get_shortcut_data' """ - for qshortcut, name, default in widget.get_shortcut_data(): - self.register_shortcut(qshortcut, context, name, default) - + for qshortcut, context, name in widget.get_shortcut_data(): + self.register_shortcut(qshortcut, context, name) + def switch_to_plugin(self): """Switch to plugin This method is called when pressing plugin's shortcut key""" @@ -271,16 +439,43 @@ def get_option(self, option, default=NoDefault): """Get a plugin option from configuration file""" return CONF.get(self.CONF_SECTION, option, default) - - def get_plugin_font(self, option=None): - """Return plugin font option""" - return get_font(self.CONF_SECTION, option) - - def set_plugin_font(self, font, option=None): - """Set plugin font option""" - set_font(font, self.CONF_SECTION, option) - - def show_message(self, message, timeout=0): + + def get_plugin_font(self, rich_text=False): + """ + Return plugin font option. + + All plugins in Spyder use a global font. This is a convenience method + in case some plugins will have a delta size based on the default size. + """ + + if rich_text: + option = 'rich_font' + font_size_delta = self.RICH_FONT_SIZE_DELTA + else: + option = 'font' + font_size_delta = self.FONT_SIZE_DELTA + + return get_font(option=option, font_size_delta=font_size_delta) + + def set_plugin_font(self): + """ + Set plugin font option. + + Note: All plugins in Spyder use a global font. To define a different + size, the plugin must define a 'FONT_SIZE_DELTA' class variable. + """ + raise Exception("Plugins font is based on the general settings, " + "and cannot be set directly on the plugin." + "This method is deprecated.") + + def update_font(self): + """ + This has to be reimplemented by plugins that need to adjust + their fonts + """ + pass + + def __show_message(self, message, timeout=0): """Show message in main window's status bar""" self.main.statusBar().showMessage(message, timeout) @@ -289,7 +484,7 @@ Showing message in main window's status bar and changing mouse cursor to Qt.WaitCursor """ - self.show_message(message) + self.__show_message(message) QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) QApplication.processEvents() @@ -299,33 +494,32 @@ and restoring mouse cursor """ QApplication.restoreOverrideCursor() - self.show_message(message, timeout=2000) + self.__show_message(message, timeout=2000) QApplication.processEvents() - - def set_default_color_scheme(self, name='Spyder'): - """Set default color scheme (only once)""" - color_scheme_name = self.get_option('color_scheme_name', None) - if color_scheme_name is None: - names = CONF.get("color_schemes", "names") - if name not in names: - name = names[0] - self.set_option('color_scheme_name', name) - + + def get_color_scheme(self): + """Get current color scheme""" + return get_color_scheme(CONF.get('color_schemes', 'selected')) + def create_toggle_view_action(self): """Associate a toggle view action with each plugin""" title = self.get_plugin_title() if self.CONF_SECTION == 'editor': title = _('Editor') if self.shortcut is not None: - action = create_action(self, title, toggled=self.toggle_view, - shortcut=QKeySequence(self.shortcut)) - action.setShortcutContext(Qt.WidgetWithChildrenShortcut) + action = create_action(self, title, + toggled=lambda checked: self.toggle_view(checked), + shortcut=QKeySequence(self.shortcut), + context=Qt.WidgetShortcut) else: - action = create_action(self, title, toggled=self.toggle_view) + action = create_action(self, title, toggled=lambda checked: + self.toggle_view(checked)) self.toggle_view_action = action def toggle_view(self, checked): """Toggle view""" + if not self.dockwidget: + return if checked: self.dockwidget.show() self.dockwidget.raise_() @@ -339,10 +533,16 @@ Spyder's widgets either inherit this class or reimplement its interface """ sig_option_changed = Signal(str, object) - - def __init__(self, parent): - QWidget.__init__(self, parent) - SpyderPluginMixin.__init__(self, parent) + show_message = Signal(str, int) + update_plugin_title = Signal() + + if PYQT5: + def __init__(self, parent, **kwds): + super(SpyderPluginWidget, self).__init__(parent, **kwds) + else: + def __init__(self, parent): + QWidget.__init__(self, parent) + SpyderPluginMixin.__init__(self, parent) def get_plugin_title(self): """ @@ -359,7 +559,7 @@ (see SpyderPluginMixin.create_mainwindow) and for configuration dialog widgets creation """ - return get_icon('qt.png') + return ima.icon('outline_explorer') def get_focus_widget(self): """ @@ -374,7 +574,7 @@ Return True or False whether the plugin may be closed immediately or not Note: returned value is ignored if *cancelable* is False """ - raise NotImplementedError + return True def refresh_plugin(self): """Refresh widget""" diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/plugins/inspector.py spyder-3.0.2+dfsg1/spyder/plugins/inspector.py --- spyder-2.3.8+dfsg1/spyder/plugins/inspector.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/plugins/inspector.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,1005 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright © 2009-2010 Pierre Raybaut -# Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) - -"""Object Inspector Plugin""" - -from spyderlib.qt.QtGui import (QHBoxLayout, QVBoxLayout, QLabel, QSizePolicy, - QMenu, QToolButton, QGroupBox, QFontComboBox, - QActionGroup, QFontDialog, QWidget, QComboBox, - QLineEdit, QMessageBox) -from spyderlib.qt.QtCore import SIGNAL, QUrl, QThread -from spyderlib.qt.QtWebKit import QWebPage - -import re -import os.path as osp -import socket -import sys - -# Local imports -from spyderlib import dependencies -from spyderlib.baseconfig import get_conf_path, get_module_source_path, _ -from spyderlib.ipythonconfig import IPYTHON_QT_INSTALLED -from spyderlib.config import CONF -from spyderlib.guiconfig import get_color_scheme, get_font, set_font -from spyderlib.utils import programs -from spyderlib.utils.qthelpers import (get_icon, create_toolbutton, - add_actions, create_action) -from spyderlib.widgets.comboboxes import EditableComboBox -from spyderlib.widgets.sourcecode import codeeditor -from spyderlib.widgets.findreplace import FindReplace -from spyderlib.widgets.browser import WebView -from spyderlib.widgets.externalshell.pythonshell import ExtPythonShellWidget -from spyderlib.plugins import SpyderPluginWidget, PluginConfigPage -from spyderlib.py3compat import to_text_string, get_meth_class_inst - -#XXX: Hardcoded dependency on optional IPython plugin component -# that requires the hack to make this work without IPython -if IPYTHON_QT_INSTALLED: - from spyderlib.widgets.ipython import IPythonControlWidget -else: - IPythonControlWidget = None # analysis:ignore - -# Check if we can import Sphinx to activate rich text mode -try: - from spyderlib.utils.inspector.sphinxify import (CSS_PATH, sphinxify, - warning, generate_context, - usage) - sphinx_version = programs.get_module_version('sphinx') -except (ImportError, TypeError): - sphinxify = sphinx_version = None # analysis:ignore - -# To add sphinx dependency to the Dependencies dialog -SPHINX_REQVER = '>=0.6.6' -dependencies.add("sphinx", _("Rich text help on the Object Inspector"), - required_version=SPHINX_REQVER) - - -class ObjectComboBox(EditableComboBox): - """ - QComboBox handling object names - """ - def __init__(self, parent): - EditableComboBox.__init__(self, parent) - self.object_inspector = parent - self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) - self.tips = {True: '', False: ''} - - def is_valid(self, qstr=None): - """Return True if string is valid""" - if not self.object_inspector.source_is_console(): - return True - if qstr is None: - qstr = self.currentText() - if not re.search('^[a-zA-Z0-9_\.]*$', str(qstr), 0): - return False - objtxt = to_text_string(qstr) - if self.object_inspector.get_option('automatic_import'): - shell = self.object_inspector.internal_shell - if shell is not None: - return shell.is_defined(objtxt, force_import=True) - shell = self.object_inspector.get_shell() - if shell is not None: - try: - return shell.is_defined(objtxt) - except socket.error: - shell = self.object_inspector.get_shell() - try: - return shell.is_defined(objtxt) - except socket.error: - # Well... too bad! - pass - - def validate_current_text(self): - self.validate(self.currentText()) - - def validate(self, qstr, editing=True): - """Reimplemented to avoid formatting actions""" - valid = self.is_valid(qstr) - if self.hasFocus() and valid is not None: - if editing: - # Combo box text is being modified: invalidate the entry - self.show_tip(self.tips[valid]) - self.emit(SIGNAL('valid(bool)'), False) - else: - # A new item has just been selected - if valid: - self.selected() - else: - self.emit(SIGNAL('valid(bool)'), False) - else: - self.set_default_style() - - -class ObjectInspectorConfigPage(PluginConfigPage): - def setup_page(self): - # Fonts group - plain_text_font_group = self.create_fontgroup(option=None, - text=_("Plain text font style"), - fontfilters=QFontComboBox.MonospacedFonts) - rich_text_font_group = self.create_fontgroup(option='rich_text', - text=_("Rich text font style")) - - # Connections group - connections_group = QGroupBox(_("Automatic connections")) - connections_label = QLabel(_("The Object Inspector can automatically " - "show an object's help information after " - "a left parenthesis is written next to it. " - "Below you can decide to which plugin " - "you want to connect it to turn on this " - "feature.")) - connections_label.setWordWrap(True) - editor_box = self.create_checkbox(_("Editor"), 'connect/editor') - rope_installed = programs.is_module_installed('rope') - jedi_installed = programs.is_module_installed('jedi', '>=0.8.1') - editor_box.setEnabled(rope_installed or jedi_installed) - if not rope_installed and not jedi_installed: - editor_tip = _("This feature requires the Rope or Jedi libraries.\n" - "It seems you don't have either installed.") - editor_box.setToolTip(editor_tip) - python_box = self.create_checkbox(_("Python Console"), - 'connect/python_console') - ipython_box = self.create_checkbox(_("IPython Console"), - 'connect/ipython_console') - ipython_box.setEnabled(IPYTHON_QT_INSTALLED) - - connections_layout = QVBoxLayout() - connections_layout.addWidget(connections_label) - connections_layout.addWidget(editor_box) - connections_layout.addWidget(python_box) - connections_layout.addWidget(ipython_box) - connections_group.setLayout(connections_layout) - - # Features group - features_group = QGroupBox(_("Additional features")) - math_box = self.create_checkbox(_("Render mathematical equations"), - 'math') - req_sphinx = sphinx_version is not None and \ - programs.is_module_installed('sphinx', '>=1.1') - math_box.setEnabled(req_sphinx) - if not req_sphinx: - sphinx_tip = _("This feature requires Sphinx 1.1 or superior.") - if sphinx_version is not None: - sphinx_tip += "\n" + _("Sphinx %s is currently installed." - ) % sphinx_version - math_box.setToolTip(sphinx_tip) - - features_layout = QVBoxLayout() - features_layout.addWidget(math_box) - features_group.setLayout(features_layout) - - # Source code group - sourcecode_group = QGroupBox(_("Source code")) - wrap_mode_box = self.create_checkbox(_("Wrap lines"), 'wrap') - names = CONF.get('color_schemes', 'names') - choices = list(zip(names, names)) - cs_combo = self.create_combobox(_("Syntax color scheme: "), - choices, 'color_scheme_name') - - sourcecode_layout = QVBoxLayout() - sourcecode_layout.addWidget(wrap_mode_box) - sourcecode_layout.addWidget(cs_combo) - sourcecode_group.setLayout(sourcecode_layout) - - # Final layout - vlayout = QVBoxLayout() - vlayout.addWidget(rich_text_font_group) - vlayout.addWidget(plain_text_font_group) - vlayout.addWidget(connections_group) - vlayout.addWidget(features_group) - vlayout.addWidget(sourcecode_group) - vlayout.addStretch(1) - self.setLayout(vlayout) - - -class RichText(QWidget): - """ - WebView widget with find dialog - """ - def __init__(self, parent): - QWidget.__init__(self, parent) - - self.webview = WebView(self) - self.find_widget = FindReplace(self) - self.find_widget.set_editor(self.webview) - self.find_widget.hide() - - layout = QVBoxLayout() - layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(self.webview) - layout.addWidget(self.find_widget) - self.setLayout(layout) - - def set_font(self, font, fixed_font=None): - """Set font""" - self.webview.set_font(font, fixed_font=fixed_font) - - def set_html(self, html_text, base_url): - """Set html text""" - self.webview.setHtml(html_text, base_url) - - def clear(self): - self.set_html('', self.webview.url()) - - -class PlainText(QWidget): - """ - Read-only editor widget with find dialog - """ - def __init__(self, parent): - QWidget.__init__(self, parent) - self.editor = None - - # Read-only editor - self.editor = codeeditor.CodeEditor(self) - self.editor.setup_editor(linenumbers=False, language='py', - scrollflagarea=False, edge_line=False) - self.connect(self.editor, SIGNAL("focus_changed()"), - lambda: self.emit(SIGNAL("focus_changed()"))) - self.editor.setReadOnly(True) - - # Find/replace widget - self.find_widget = FindReplace(self) - self.find_widget.set_editor(self.editor) - self.find_widget.hide() - - layout = QVBoxLayout() - layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(self.editor) - layout.addWidget(self.find_widget) - self.setLayout(layout) - - def set_font(self, font, color_scheme=None): - """Set font""" - self.editor.set_font(font, color_scheme=color_scheme) - - def set_color_scheme(self, color_scheme): - """Set color scheme""" - self.editor.set_color_scheme(color_scheme) - - def set_text(self, text, is_code): - self.editor.set_highlight_current_line(is_code) - self.editor.set_occurence_highlighting(is_code) - if is_code: - self.editor.set_language('py') - else: - self.editor.set_language(None) - self.editor.set_text(text) - self.editor.set_cursor_position('sof') - - def clear(self): - self.editor.clear() - - -class SphinxThread(QThread): - """ - A worker thread for handling rich text rendering. - - Parameters - ---------- - doc : str or dict - A string containing a raw rst text or a dict containing - the doc string components to be rendered. - See spyderlib.utils.dochelpers.getdoc for description. - context : dict - A dict containing the substitution variables for the - layout template - html_text_no_doc : unicode - Text to be rendered if doc string cannot be extracted. - math_option : bool - Use LaTeX math rendering. - - """ - def __init__(self, html_text_no_doc=''): - super(SphinxThread, self).__init__() - self.doc = None - self.context = None - self.html_text_no_doc = html_text_no_doc - self.math_option = False - - def render(self, doc, context=None, math_option=False): - """Start thread to render a given documentation""" - # If the thread is already running wait for it to finish before - # starting it again. - if self.wait(): - self.doc = doc - self.context = context - self.math_option = math_option - # This causes run() to be executed in separate thread - self.start() - - def run(self): - html_text = self.html_text_no_doc - doc = self.doc - if doc is not None: - if type(doc) is dict and 'docstring' in doc.keys(): - try: - context = generate_context(name=doc['name'], - argspec=doc['argspec'], - note=doc['note'], - math=self.math_option) - html_text = sphinxify(doc['docstring'], context) - if doc['docstring'] == '': - html_text += '
    ' - html_text += self.html_text_no_doc - - except Exception as error: - self.emit(SIGNAL('error_msg(QString)'), - to_text_string(error)) - return - elif self.context is not None: - try: - html_text = sphinxify(doc, self.context) - except Exception as error: - self.emit(SIGNAL('error_msg(QString)'), - to_text_string(error)) - return - self.emit(SIGNAL('html_ready(QString)'), html_text) - - -class ObjectInspector(SpyderPluginWidget): - """ - Docstrings viewer widget - """ - CONF_SECTION = 'inspector' - CONFIGWIDGET_CLASS = ObjectInspectorConfigPage - LOG_PATH = get_conf_path(CONF_SECTION) - def __init__(self, parent): - SpyderPluginWidget.__init__(self, parent) - - self.internal_shell = None - - # Initialize plugin - self.initialize_plugin() - - self.no_doc_string = _("No further documentation available") - - self._last_console_cb = None - self._last_editor_cb = None - - self.set_default_color_scheme() - - self.plain_text = PlainText(self) - self.rich_text = RichText(self) - - color_scheme = get_color_scheme(self.get_option('color_scheme_name')) - self.set_plain_text_font(self.get_plugin_font(), color_scheme) - self.plain_text.editor.toggle_wrap_mode(self.get_option('wrap')) - - # Add entries to read-only editor context-menu - font_action = create_action(self, _("&Font..."), None, - 'font.png', _("Set font style"), - triggered=self.change_font) - self.wrap_action = create_action(self, _("Wrap lines"), - toggled=self.toggle_wrap_mode) - self.wrap_action.setChecked(self.get_option('wrap')) - self.plain_text.editor.readonly_menu.addSeparator() - add_actions(self.plain_text.editor.readonly_menu, - (font_action, self.wrap_action)) - - self.set_rich_text_font(self.get_plugin_font('rich_text')) - - self.shell = None - - self.external_console = None - - # locked = disable link with Console - self.locked = False - self._last_texts = [None, None] - self._last_editor_doc = None - - # Object name - layout_edit = QHBoxLayout() - layout_edit.setContentsMargins(0, 0, 0, 0) - txt = _("Source") - if sys.platform == 'darwin': - source_label = QLabel(" " + txt) - else: - source_label = QLabel(txt) - layout_edit.addWidget(source_label) - self.source_combo = QComboBox(self) - self.source_combo.addItems([_("Console"), _("Editor")]) - self.connect(self.source_combo, SIGNAL('currentIndexChanged(int)'), - self.source_changed) - if (not programs.is_module_installed('rope') and - not programs.is_module_installed('jedi', '>=0.8.1')): - self.source_combo.hide() - source_label.hide() - layout_edit.addWidget(self.source_combo) - layout_edit.addSpacing(10) - layout_edit.addWidget(QLabel(_("Object"))) - self.combo = ObjectComboBox(self) - layout_edit.addWidget(self.combo) - self.object_edit = QLineEdit(self) - self.object_edit.setReadOnly(True) - layout_edit.addWidget(self.object_edit) - self.combo.setMaxCount(self.get_option('max_history_entries')) - self.combo.addItems( self.load_history() ) - self.combo.setItemText(0, '') - self.connect(self.combo, SIGNAL("valid(bool)"), - lambda valid: self.force_refresh()) - - # Plain text docstring option - self.docstring = True - self.rich_help = sphinxify is not None \ - and self.get_option('rich_mode', True) - self.plain_text_action = create_action(self, _("Plain Text"), - toggled=self.toggle_plain_text) - - # Source code option - self.show_source_action = create_action(self, _("Show Source"), - toggled=self.toggle_show_source) - - # Rich text option - self.rich_text_action = create_action(self, _("Rich Text"), - toggled=self.toggle_rich_text) - - # Add the help actions to an exclusive QActionGroup - help_actions = QActionGroup(self) - help_actions.setExclusive(True) - help_actions.addAction(self.plain_text_action) - help_actions.addAction(self.rich_text_action) - - # Automatic import option - self.auto_import_action = create_action(self, _("Automatic import"), - toggled=self.toggle_auto_import) - auto_import_state = self.get_option('automatic_import') - self.auto_import_action.setChecked(auto_import_state) - - # Lock checkbox - self.locked_button = create_toolbutton(self, - triggered=self.toggle_locked) - layout_edit.addWidget(self.locked_button) - self._update_lock_icon() - - # Option menu - options_button = create_toolbutton(self, text=_("Options"), - icon=get_icon('tooloptions.png')) - options_button.setPopupMode(QToolButton.InstantPopup) - menu = QMenu(self) - add_actions(menu, [self.rich_text_action, self.plain_text_action, - self.show_source_action, None, - self.auto_import_action]) - options_button.setMenu(menu) - layout_edit.addWidget(options_button) - - if self.rich_help: - self.switch_to_rich_text() - else: - self.switch_to_plain_text() - self.plain_text_action.setChecked(not self.rich_help) - self.rich_text_action.setChecked(self.rich_help) - self.rich_text_action.setEnabled(sphinxify is not None) - self.source_changed() - - # Main layout - layout = QVBoxLayout() - layout.setContentsMargins(0, 0, 0, 0) - layout.addLayout(layout_edit) - layout.addWidget(self.plain_text) - layout.addWidget(self.rich_text) - self.setLayout(layout) - - # Add worker thread for handling rich text rendering - if sphinxify is None: - self._sphinx_thread = None - else: - self._sphinx_thread = SphinxThread( - html_text_no_doc=warning(self.no_doc_string)) - self.connect(self._sphinx_thread, SIGNAL('html_ready(QString)'), - self._on_sphinx_thread_html_ready) - self.connect(self._sphinx_thread, SIGNAL('error_msg(QString)'), - self._on_sphinx_thread_error_msg) - - # Render internal links - view = self.rich_text.webview - view.page().setLinkDelegationPolicy(QWebPage.DelegateAllLinks) - view.linkClicked.connect(self.handle_link_clicks) - - self._starting_up = True - - #------ SpyderPluginWidget API --------------------------------------------- - def get_plugin_title(self): - """Return widget title""" - return _('Object inspector') - - def get_plugin_icon(self): - """Return widget icon""" - return get_icon('inspector.png') - - def get_focus_widget(self): - """ - Return the widget to give focus to when - this plugin's dockwidget is raised on top-level - """ - self.combo.lineEdit().selectAll() - return self.combo - - def get_plugin_actions(self): - """Return a list of actions related to plugin""" - return [] - - def register_plugin(self): - """Register plugin in Spyder's main window""" - self.connect(self, SIGNAL('focus_changed()'), - self.main.plugin_focus_changed) - self.main.add_dockwidget(self) - self.main.console.set_inspector(self) - self.internal_shell = self.main.console.shell - - def closing_plugin(self, cancelable=False): - """Perform actions before parent main window is closed""" - return True - - def refresh_plugin(self): - """Refresh widget""" - if self._starting_up: - self._starting_up = False - if sphinxify is not None: - self.switch_to_rich_text() - self.show_intro_message() - - def apply_plugin_settings(self, options): - """Apply configuration file's plugin settings""" - color_scheme_n = 'color_scheme_name' - color_scheme_o = get_color_scheme(self.get_option(color_scheme_n)) - font_n = 'plugin_font' - font_o = self.get_plugin_font() - connect_n = 'connect_to_oi' - rich_font_n = 'rich_text' - rich_font_o = self.get_plugin_font('rich_text') - wrap_n = 'wrap' - wrap_o = self.get_option(wrap_n) - self.wrap_action.setChecked(wrap_o) - math_n = 'math' - math_o = self.get_option(math_n) - - if font_n in options: - scs = color_scheme_o if color_scheme_n in options else None - self.set_plain_text_font(font_o, color_scheme=scs) - if rich_font_n in options: - self.set_rich_text_font(rich_font_o) - elif color_scheme_n in options: - self.set_plain_text_color_scheme(color_scheme_o) - if wrap_n in options: - self.toggle_wrap_mode(wrap_o) - if math_n in options: - self.toggle_math_mode(math_o) - - # To make auto-connection changes take place instantly - self.main.editor.apply_plugin_settings(options=[connect_n]) - self.main.extconsole.apply_plugin_settings(options=[connect_n]) - if self.main.ipyconsole is not None: - self.main.ipyconsole.apply_plugin_settings(options=[connect_n]) - - #------ Public API (related to inspector's source) ------------------------- - def source_is_console(self): - """Return True if source is Console""" - return self.source_combo.currentIndex() == 0 - - def switch_to_editor_source(self): - self.source_combo.setCurrentIndex(1) - - def switch_to_console_source(self): - self.source_combo.setCurrentIndex(0) - - def source_changed(self, index=None): - if self.source_is_console(): - # Console - self.combo.show() - self.object_edit.hide() - self.show_source_action.setEnabled(True) - self.auto_import_action.setEnabled(True) - else: - # Editor - self.combo.hide() - self.object_edit.show() - self.show_source_action.setDisabled(True) - self.auto_import_action.setDisabled(True) - self.restore_text() - - def save_text(self, callback): - if self.source_is_console(): - self._last_console_cb = callback - else: - self._last_editor_cb = callback - - def restore_text(self): - if self.source_is_console(): - cb = self._last_console_cb - else: - cb = self._last_editor_cb - if cb is None: - if self.is_plain_text_mode(): - self.plain_text.clear() - else: - self.rich_text.clear() - else: - func = cb[0] - args = cb[1:] - func(*args) - if get_meth_class_inst(func) is self.rich_text: - self.switch_to_rich_text() - else: - self.switch_to_plain_text() - - #------ Public API (related to rich/plain text widgets) -------------------- - @property - def find_widget(self): - if self.plain_text.isVisible(): - return self.plain_text.find_widget - else: - return self.rich_text.find_widget - - def set_rich_text_font(self, font): - """Set rich text mode font""" - self.rich_text.set_font(font, fixed_font=self.get_plugin_font()) - - def set_plain_text_font(self, font, color_scheme=None): - """Set plain text mode font""" - self.plain_text.set_font(font, color_scheme=color_scheme) - - def set_plain_text_color_scheme(self, color_scheme): - """Set plain text mode color scheme""" - self.plain_text.set_color_scheme(color_scheme) - - def change_font(self): - """Change console font""" - font, valid = QFontDialog.getFont(get_font(self.CONF_SECTION), self, - _("Select a new font")) - if valid: - self.set_plain_text_font(font) - set_font(font, self.CONF_SECTION) - - def toggle_wrap_mode(self, checked): - """Toggle wrap mode""" - self.plain_text.editor.toggle_wrap_mode(checked) - self.set_option('wrap', checked) - - def toggle_math_mode(self, checked): - """Toggle math mode""" - self.set_option('math', checked) - - def is_plain_text_mode(self): - """Return True if plain text mode is active""" - return self.plain_text.isVisible() - - def is_rich_text_mode(self): - """Return True if rich text mode is active""" - return self.rich_text.isVisible() - - def switch_to_plain_text(self): - """Switch to plain text mode""" - self.rich_help = False - self.plain_text.show() - self.rich_text.hide() - self.plain_text_action.setChecked(True) - - def switch_to_rich_text(self): - """Switch to rich text mode""" - self.rich_help = True - self.plain_text.hide() - self.rich_text.show() - self.rich_text_action.setChecked(True) - self.show_source_action.setChecked(False) - - def set_plain_text(self, text, is_code): - """Set plain text docs""" - - # text is coming from utils.dochelpers.getdoc - if type(text) is dict: - name = text['name'] - if name: - rst_title = ''.join(['='*len(name), '\n', name, '\n', - '='*len(name), '\n\n']) - else: - rst_title = '' - - if text['argspec']: - definition = ''.join(['Definition: ', name, text['argspec'], - '\n']) - else: - definition = '' - - if text['note']: - note = ''.join(['Type: ', text['note'], '\n\n----\n\n']) - else: - note = '' - - full_text = ''.join([rst_title, definition, note, - text['docstring']]) - else: - full_text = text - - self.plain_text.set_text(full_text, is_code) - self.save_text([self.plain_text.set_text, full_text, is_code]) - - def set_rich_text_html(self, html_text, base_url): - """Set rich text""" - self.rich_text.set_html(html_text, base_url) - self.save_text([self.rich_text.set_html, html_text, base_url]) - - def show_intro_message(self): - intro_message = _("Here you can get help of any object by pressing " - "%s in front of it, either on the Editor or the " - "Console.%s" - "Help can also be shown automatically after writing " - "a left parenthesis next to an object. You can " - "activate this behavior in %s.") - prefs = _("Preferences > Object Inspector") - if self.is_rich_text_mode(): - title = _("Usage") - tutorial_message = _("New to Spyder? Read our") - tutorial = _("tutorial") - intro_message = intro_message % ("Ctrl+I", "

    ", - ""+prefs+"") - self.set_rich_text_html(usage(title, intro_message, - tutorial_message, tutorial), - QUrl.fromLocalFile(CSS_PATH)) - else: - install_sphinx = "\n\n%s" % _("Please consider installing Sphinx " - "to get documentation rendered in " - "rich text.") - intro_message = intro_message % ("Ctrl+I", "\n\n", prefs) - intro_message += install_sphinx - self.set_plain_text(intro_message, is_code=False) - - def show_rich_text(self, text, collapse=False, img_path=''): - """Show text in rich mode""" - self.visibility_changed(True) - self.raise_() - self.switch_to_rich_text() - context = generate_context(collapse=collapse, img_path=img_path) - self.render_sphinx_doc(text, context) - - def show_plain_text(self, text): - """Show text in plain mode""" - self.visibility_changed(True) - self.raise_() - self.switch_to_plain_text() - self.set_plain_text(text, is_code=False) - - def show_tutorial(self): - tutorial_path = get_module_source_path('spyderlib.utils.inspector') - img_path = osp.join(tutorial_path, 'static', 'images') - tutorial = osp.join(tutorial_path, 'tutorial.rst') - text = open(tutorial).read() - if sphinxify is not None: - self.show_rich_text(text, collapse=True, img_path=img_path) - else: - self.show_plain_text(text) - - def handle_link_clicks(self, url): - url = to_text_string(url.toString()) - if url == "spy://tutorial": - self.show_tutorial() - elif url.startswith('http'): - programs.start_file(url) - else: - self.rich_text.webview.load(QUrl(url)) - - #------ Public API --------------------------------------------------------- - def set_external_console(self, external_console): - self.external_console = external_console - - def force_refresh(self): - if self.source_is_console(): - self.set_object_text(None, force_refresh=True) - elif self._last_editor_doc is not None: - self.set_editor_doc(self._last_editor_doc, force_refresh=True) - - def set_object_text(self, text, force_refresh=False, ignore_unknown=False): - """Set object analyzed by Object Inspector""" - if (self.locked and not force_refresh): - return - self.switch_to_console_source() - - add_to_combo = True - if text is None: - text = to_text_string(self.combo.currentText()) - add_to_combo = False - - found = self.show_help(text, ignore_unknown=ignore_unknown) - if ignore_unknown and not found: - return - - if add_to_combo: - self.combo.add_text(text) - if found: - self.save_history() - - if self.dockwidget is not None: - self.dockwidget.blockSignals(True) - self.__eventually_raise_inspector(text, force=force_refresh) - if self.dockwidget is not None: - self.dockwidget.blockSignals(False) - - def set_editor_doc(self, doc, force_refresh=False): - """ - Use the object inspector to show docstring dictionary computed - with introspection plugin from the Editor plugin - """ - if (self.locked and not force_refresh): - return - self.switch_to_editor_source() - self._last_editor_doc = doc - self.object_edit.setText(doc['obj_text']) - - if self.rich_help: - self.render_sphinx_doc(doc) - else: - self.set_plain_text(doc, is_code=False) - - if self.dockwidget is not None: - self.dockwidget.blockSignals(True) - self.__eventually_raise_inspector(doc['docstring'], - force=force_refresh) - if self.dockwidget is not None: - self.dockwidget.blockSignals(False) - - def __eventually_raise_inspector(self, text, force=False): - index = self.source_combo.currentIndex() - if hasattr(self.main, 'tabifiedDockWidgets'): - # 'QMainWindow.tabifiedDockWidgets' was introduced in PyQt 4.5 - if self.dockwidget and (force or self.dockwidget.isVisible()) \ - and not self.ismaximized \ - and (force or text != self._last_texts[index]): - dockwidgets = self.main.tabifiedDockWidgets(self.dockwidget) - if self.main.console.dockwidget not in dockwidgets and \ - (hasattr(self.main, 'extconsole') and \ - self.main.extconsole.dockwidget not in dockwidgets): - self.dockwidget.show() - self.dockwidget.raise_() - self._last_texts[index] = text - - def load_history(self, obj=None): - """Load history from a text file in user home directory""" - if osp.isfile(self.LOG_PATH): - history = [line.replace('\n', '') - for line in open(self.LOG_PATH, 'r').readlines()] - else: - history = [] - return history - - def save_history(self): - """Save history to a text file in user home directory""" - open(self.LOG_PATH, 'w').write("\n".join( \ - [to_text_string(self.combo.itemText(index)) - for index in range(self.combo.count())] )) - - def toggle_plain_text(self, checked): - """Toggle plain text docstring""" - if checked: - self.docstring = checked - self.switch_to_plain_text() - self.force_refresh() - self.set_option('rich_mode', not checked) - - def toggle_show_source(self, checked): - """Toggle show source code""" - if checked: - self.switch_to_plain_text() - self.docstring = not checked - self.force_refresh() - self.set_option('rich_mode', not checked) - - def toggle_rich_text(self, checked): - """Toggle between sphinxified docstrings or plain ones""" - if checked: - self.docstring = not checked - self.switch_to_rich_text() - self.set_option('rich_mode', checked) - - def toggle_auto_import(self, checked): - """Toggle automatic import feature""" - self.combo.validate_current_text() - self.set_option('automatic_import', checked) - self.force_refresh() - - def toggle_locked(self): - """ - Toggle locked state - locked = disable link with Console - """ - self.locked = not self.locked - self._update_lock_icon() - - def _update_lock_icon(self): - """Update locked state icon""" - icon = get_icon("lock.png" if self.locked else "lock_open.png") - self.locked_button.setIcon(icon) - tip = _("Unlock") if self.locked else _("Lock") - self.locked_button.setToolTip(tip) - - def set_shell(self, shell): - """Bind to shell""" - if IPythonControlWidget is not None: - # XXX(anatoli): hack to make Spyder run on systems without IPython - # there should be a better way - if isinstance(shell, IPythonControlWidget): - # XXX: this ignores passed argument completely - self.shell = self.external_console.get_current_shell() - else: - self.shell = shell - - def get_shell(self): - """Return shell which is currently bound to object inspector, - or another running shell if it has been terminated""" - if not isinstance(self.shell, ExtPythonShellWidget) \ - or not self.shell.externalshell.is_running(): - self.shell = None - if self.external_console is not None: - self.shell = self.external_console.get_running_python_shell() - if self.shell is None: - self.shell = self.internal_shell - return self.shell - - def render_sphinx_doc(self, doc, context=None): - """Transform doc string dictionary to HTML and show it""" - # Math rendering option could have changed - self._sphinx_thread.render(doc, context, self.get_option('math')) - - def _on_sphinx_thread_html_ready(self, html_text): - """Set our sphinx documentation based on thread result""" - self._sphinx_thread.wait() - self.set_rich_text_html(html_text, QUrl.fromLocalFile(CSS_PATH)) - - def _on_sphinx_thread_error_msg(self, error_msg): - """ Display error message on Sphinx rich text failure""" - self._sphinx_thread.wait() - self.plain_text_action.setChecked(True) - QMessageBox.critical(self, - _('Object inspector'), - _("The following error occured when calling " - "Sphinx %s.
    Incompatible Sphinx " - "version or doc string decoding failed." - "

    Error message:
    %s" - ) % (sphinx_version, error_msg)) - - def show_help(self, obj_text, ignore_unknown=False): - """Show help""" - shell = self.get_shell() - if shell is None: - return - obj_text = to_text_string(obj_text) - - if not shell.is_defined(obj_text): - if self.get_option('automatic_import') and\ - self.internal_shell.is_defined(obj_text, force_import=True): - shell = self.internal_shell - else: - shell = None - doc = None - source_text = None - - if shell is not None: - doc = shell.get_doc(obj_text) - source_text = shell.get_source(obj_text) - - is_code = False - - if self.rich_help: - self.render_sphinx_doc(doc) - return doc is not None - elif self.docstring: - hlp_text = doc - if hlp_text is None: - hlp_text = source_text - if hlp_text is None: - hlp_text = self.no_doc_string - if ignore_unknown: - return False - else: - hlp_text = source_text - if hlp_text is None: - hlp_text = doc - if hlp_text is None: - hlp_text = _("No source code available.") - if ignore_unknown: - return False - else: - is_code = True - self.set_plain_text(hlp_text, is_code=is_code) - return True diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/plugins/ipythonconsole.py spyder-3.0.2+dfsg1/spyder/plugins/ipythonconsole.py --- spyder-2.3.8+dfsg1/spyder/plugins/ipythonconsole.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/plugins/ipythonconsole.py 2016-11-19 01:31:58.000000000 +0100 @@ -1,67 +1,73 @@ # -*- coding: utf-8 -*- # -# Copyright © 2012 Pierre Raybaut +# Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) +# (see spyder/__init__.py for details) -"""IPython Console plugin - -Handles IPython clients (and in the future, will handle IPython kernels too --- meanwhile, the external console plugin is handling them)""" +""" +IPython Console plugin based on QtConsole +""" # pylint: disable=C0103 # pylint: disable=R0903 # pylint: disable=R0911 # pylint: disable=R0201 -# Stdlib imports +# Standard library imports import atexit import os import os.path as osp +import uuid import sys -# Qt imports -from spyderlib.qt.QtGui import (QVBoxLayout, QHBoxLayout, QFormLayout, - QMessageBox, QGroupBox, QDialogButtonBox, - QDialog, QTabWidget, QFontComboBox, - QCheckBox, QApplication, QLabel,QLineEdit, - QPushButton, QKeySequence, QWidget) -from spyderlib.qt.compat import getopenfilename -from spyderlib.qt.QtCore import SIGNAL, Qt - -# IPython imports -from IPython.core.application import get_ipython_dir -from IPython.kernel.connect import find_connection_file -from IPython.qt.manager import QtKernelManager -try: # IPython = "<=2.0" - from IPython.external.ssh import tunnel as zmqtunnel - import IPython.external.pexpect as pexpect +# Third party imports +from jupyter_client.connect import find_connection_file +from jupyter_client.kernelspec import KernelSpec +from jupyter_core.paths import jupyter_config_dir, jupyter_runtime_dir +from qtconsole.client import QtKernelClient +from qtconsole.manager import QtKernelManager +from qtpy import PYQT5 +from qtpy.compat import getopenfilename +from qtpy.QtCore import Qt, Signal, Slot +from qtpy.QtGui import QKeySequence +from qtpy.QtWidgets import (QApplication, QCheckBox, QDialog, QDialogButtonBox, + QFormLayout, QGridLayout, QGroupBox, QHBoxLayout, + QLabel, QLineEdit, QMessageBox, QPushButton, + QTabWidget, QVBoxLayout, QWidget) +from traitlets.config.loader import Config, load_pyconfig_files +from zmq.ssh import tunnel as zmqtunnel +try: + import pexpect except ImportError: - from zmq.ssh import tunnel as zmqtunnel # analysis:ignore - try: - import pexpect # analysis:ignore - except ImportError: - pexpect = None # analysis:ignore + pexpect = None # Local imports -from spyderlib import dependencies -from spyderlib.baseconfig import _ -from spyderlib.config import CONF -from spyderlib.utils.misc import get_error_match, remove_backslashes -from spyderlib.utils import programs -from spyderlib.utils.qthelpers import get_icon, create_action -from spyderlib.widgets.tabs import Tabs -from spyderlib.widgets.ipython import IPythonClient -from spyderlib.widgets.findreplace import FindReplace -from spyderlib.plugins import SpyderPluginWidget, PluginConfigPage -from spyderlib.py3compat import to_text_string +from spyder import dependencies +from spyder.config.base import (_, DEV, get_home_dir, get_module_path, + get_module_source_path) +from spyder.config.main import CONF +from spyder.plugins import SpyderPluginWidget +from spyder.plugins.configdialog import PluginConfigPage +from spyder.py3compat import (iteritems, PY2, to_binary_string, + to_text_string) +from spyder.utils.qthelpers import create_action +from spyder.utils import icon_manager as ima +from spyder.utils import encoding, programs +from spyder.utils.misc import (add_pathlist_to_PYTHONPATH, get_error_match, + get_python_executable, remove_backslashes) +from spyder.widgets.findreplace import FindReplace +from spyder.widgets.ipythonconsole import ClientWidget +from spyder.widgets.tabs import Tabs SYMPY_REQVER = '>=0.7.3' dependencies.add("sympy", _("Symbolic mathematics in the IPython Console"), - required_version=SYMPY_REQVER) + required_version=SYMPY_REQVER, optional=True) +#------------------------------------------------------------------------------ +# Existing kernels +#------------------------------------------------------------------------------ # Replacing pyzmq openssh_tunnel method to work around the issue # https://github.com/zeromq/pyzmq/issues/589 which was solved in pyzmq # https://github.com/zeromq/pyzmq/pull/615 @@ -143,29 +149,131 @@ failed = True -class IPythonConsoleConfigPage(PluginConfigPage): +class KernelConnectionDialog(QDialog): + """Dialog to connect to existing kernels (either local or remote)""" + def __init__(self, parent=None): + super(KernelConnectionDialog, self).__init__(parent) + self.setWindowTitle(_('Connect to an existing kernel')) + + main_label = QLabel(_("Please enter the connection info of the kernel " + "you want to connect to. For that you can " + "either select its JSON connection file using " + "the Browse button, or write directly " + "its id, in case it's a local kernel (for " + "example kernel-3764.json or just " + "3764).")) + main_label.setWordWrap(True) + main_label.setAlignment(Qt.AlignJustify) + + # connection file + cf_label = QLabel(_('Connection info:')) + self.cf = QLineEdit() + self.cf.setPlaceholderText(_('Path to connection file or kernel id')) + self.cf.setMinimumWidth(250) + cf_open_btn = QPushButton(_('Browse')) + cf_open_btn.clicked.connect(self.select_connection_file) + + cf_layout = QHBoxLayout() + cf_layout.addWidget(cf_label) + cf_layout.addWidget(self.cf) + cf_layout.addWidget(cf_open_btn) + + # remote kernel checkbox + self.rm_cb = QCheckBox(_('This is a remote kernel')) + + # ssh connection + self.hn = QLineEdit() + self.hn.setPlaceholderText(_('username@hostname:port')) + + self.kf = QLineEdit() + self.kf.setPlaceholderText(_('Path to ssh key file')) + kf_open_btn = QPushButton(_('Browse')) + kf_open_btn.clicked.connect(self.select_ssh_key) + + kf_layout = QHBoxLayout() + kf_layout.addWidget(self.kf) + kf_layout.addWidget(kf_open_btn) + + self.pw = QLineEdit() + self.pw.setPlaceholderText(_('Password or ssh key passphrase')) + self.pw.setEchoMode(QLineEdit.Password) + + ssh_form = QFormLayout() + ssh_form.addRow(_('Host name'), self.hn) + ssh_form.addRow(_('Ssh key'), kf_layout) + ssh_form.addRow(_('Password'), self.pw) + + # Ok and Cancel buttons + accept_btns = QDialogButtonBox( + QDialogButtonBox.Ok | QDialogButtonBox.Cancel, + Qt.Horizontal, self) + + accept_btns.accepted.connect(self.accept) + accept_btns.rejected.connect(self.reject) + + # Dialog layout + layout = QVBoxLayout(self) + layout.addWidget(main_label) + layout.addLayout(cf_layout) + layout.addWidget(self.rm_cb) + layout.addLayout(ssh_form) + layout.addWidget(accept_btns) + + # remote kernel checkbox enables the ssh_connection_form + def ssh_set_enabled(state): + for wid in [self.hn, self.kf, kf_open_btn, self.pw]: + wid.setEnabled(state) + for i in range(ssh_form.rowCount()): + ssh_form.itemAt(2 * i).widget().setEnabled(state) + + ssh_set_enabled(self.rm_cb.checkState()) + self.rm_cb.stateChanged.connect(ssh_set_enabled) + + def select_connection_file(self): + cf = getopenfilename(self, _('Open connection file'), + jupyter_runtime_dir(), '*.json;;*.*')[0] + self.cf.setText(cf) + + def select_ssh_key(self): + kf = getopenfilename(self, _('Select ssh key'), + get_home_dir(), '*.pem;;*.*')[0] + self.kf.setText(kf) + + @staticmethod + def get_connection_parameters(parent=None): + dialog = KernelConnectionDialog(parent) + result = dialog.exec_() + is_remote = bool(dialog.rm_cb.checkState()) + accepted = result == QDialog.Accepted + if is_remote: + falsy_to_none = lambda arg: arg if arg else None + return (dialog.cf.text(), # connection file + falsy_to_none(dialog.hn.text()), # host name + falsy_to_none(dialog.kf.text()), # ssh key file + falsy_to_none(dialog.pw.text()), # ssh password + accepted) # ok + else: + return (dialog.cf.text(), None, None, None, accepted) + + +#------------------------------------------------------------------------------ +# Config page +#------------------------------------------------------------------------------ +class IPythonConsoleConfigPage(PluginConfigPage): + def __init__(self, plugin, parent): PluginConfigPage.__init__(self, plugin, parent) self.get_name = lambda: _("IPython console") def setup_page(self): newcb = self.create_checkbox - mpl_present = programs.is_module_installed("matplotlib") - # --- Display --- - font_group = self.create_fontgroup(option=None, text=None, - fontfilters=QFontComboBox.MonospacedFonts) - # Interface Group interface_group = QGroupBox(_("Interface")) banner_box = newcb(_("Display initial banner"), 'show_banner', tip=_("This option lets you hide the message shown at\n" "the top of the console when it's opened.")) - gui_comp_box = newcb(_("Use a completion widget"), - 'use_gui_completion', - tip=_("Use a widget instead of plain text " - "output for tab completion")) pager_box = newcb(_("Use a pager to display additional text inside " "the console"), 'use_pager', tip=_("Useful if you don't want to fill the " @@ -178,12 +286,22 @@ interface_layout = QVBoxLayout() interface_layout.addWidget(banner_box) - interface_layout.addWidget(gui_comp_box) interface_layout.addWidget(pager_box) interface_layout.addWidget(calltips_box) interface_layout.addWidget(ask_box) interface_group.setLayout(interface_layout) + comp_group = QGroupBox(_("Completion Type")) + comp_label = QLabel(_("Decide what type of completion to use")) + comp_label.setWordWrap(True) + completers = [(_("Graphical"), 0), (_("Terminal"), 1), (_("Plain"), 2)] + comp_box = self.create_combobox(_("Completion:")+" ", completers, + 'completion_type') + comp_layout = QVBoxLayout() + comp_layout.addWidget(comp_label) + comp_layout.addWidget(comp_box) + comp_group.setLayout(comp_layout) + # Background Color Group bg_group = QGroupBox(_("Background color")) light_radio = self.create_radiobutton(_("Light background"), @@ -220,23 +338,14 @@ "plotting libraries different to " "Matplotlib or to develop \nGUIs with " "Spyder.")) - autoload_pylab_box.setEnabled(self.get_option('pylab') and mpl_present) - self.connect(pylab_box, SIGNAL("toggled(bool)"), - autoload_pylab_box.setEnabled) + autoload_pylab_box.setEnabled(self.get_option('pylab')) + pylab_box.toggled.connect(autoload_pylab_box.setEnabled) pylab_layout = QVBoxLayout() pylab_layout.addWidget(pylab_box) pylab_layout.addWidget(autoload_pylab_box) pylab_group.setLayout(pylab_layout) - if not mpl_present: - self.set_option('pylab', False) - self.set_option('pylab/autoload', False) - pylab_group.setEnabled(False) - pylab_tip = _("This feature requires the Matplotlib library.\n" - "It seems you don't have it installed.") - pylab_box.setToolTip(pylab_tip) - # Pylab backend Group inline = _("Inline") automatic = _("Automatic") @@ -249,16 +358,16 @@ "separate window.") % (inline, automatic)) bend_label.setWordWrap(True) - backends = [(inline, 0), (automatic, 1), ("Qt", 2)] - # TODO: Add gtk3 when 0.13 is released + backends = [(inline, 0), (automatic, 1), ("Qt5", 2), ("Qt4", 3)] + if sys.platform == 'darwin': - backends.append( ("Mac OSX", 3) ) - if programs.is_module_installed('pygtk'): - backends.append( ("Gtk", 4) ) - if programs.is_module_installed('wxPython'): - backends.append( ("Wx", 5) ) - if programs.is_module_installed('_tkinter'): - backends.append( ("Tkinter", 6) ) + backends.append( ("OS X", 4) ) + if sys.platform.startswith('linux'): + backends.append( ("Gtk3", 5) ) + backends.append( ("Gtk", 6) ) + if PY2: + backends.append( ("Wx", 7) ) + backends.append( ("Tkinter", 8) ) backends = tuple(backends) backend_box = self.create_combobox( _("Backend:")+" ", backends, @@ -270,9 +379,8 @@ backend_layout.addWidget(bend_label) backend_layout.addWidget(backend_box) backend_group.setLayout(backend_layout) - backend_group.setEnabled(self.get_option('pylab') and mpl_present) - self.connect(pylab_box, SIGNAL("toggled(bool)"), - backend_group.setEnabled) + backend_group.setEnabled(self.get_option('pylab')) + pylab_box.toggled.connect(backend_group.setEnabled) # Inline backend Group inline_group = QGroupBox(_("Inline backend")) @@ -296,16 +404,27 @@ 'pylab/inline/height', min_=1, max_=20, step=1, tip=_("Default is 4")) - inline_layout = QVBoxLayout() - inline_layout.addWidget(inline_label) - inline_layout.addWidget(format_box) - inline_layout.addWidget(resolution_spin) - inline_layout.addWidget(width_spin) - inline_layout.addWidget(height_spin) - inline_group.setLayout(inline_layout) - inline_group.setEnabled(self.get_option('pylab') and mpl_present) - self.connect(pylab_box, SIGNAL("toggled(bool)"), - inline_group.setEnabled) + inline_v_layout = QVBoxLayout() + inline_v_layout.addWidget(inline_label) + inline_layout = QGridLayout() + inline_layout.addWidget(format_box.label, 1, 0) + inline_layout.addWidget(format_box.combobox, 1, 1) + inline_layout.addWidget(resolution_spin.plabel, 2, 0) + inline_layout.addWidget(resolution_spin.spinbox, 2, 1) + inline_layout.addWidget(resolution_spin.slabel, 2, 2) + inline_layout.addWidget(width_spin.plabel, 3, 0) + inline_layout.addWidget(width_spin.spinbox, 3, 1) + inline_layout.addWidget(width_spin.slabel, 3, 2) + inline_layout.addWidget(height_spin.plabel, 4, 0) + inline_layout.addWidget(height_spin.spinbox, 4, 1) + inline_layout.addWidget(height_spin.slabel, 4, 2) + inline_h_layout = QHBoxLayout() + inline_h_layout.addLayout(inline_layout) + inline_h_layout.addStretch(1) + inline_v_layout.addLayout(inline_h_layout) + inline_group.setLayout(inline_v_layout) + inline_group.setEnabled(self.get_option('pylab')) + pylab_box.toggled.connect(inline_group.setEnabled) # --- Startup --- # Run lines Group @@ -334,8 +453,7 @@ 'startup/use_run_file', False) run_file_browser = self.create_browsefile('', 'startup/run_file', '') run_file_browser.setEnabled(False) - self.connect(file_radio, SIGNAL("toggled(bool)"), - run_file_browser.setEnabled) + file_radio.toggled.connect(run_file_browser.setEnabled) run_file_layout = QVBoxLayout() run_file_layout.addWidget(run_file_label) @@ -394,7 +512,7 @@ sympy_label = QLabel(_("Perfom symbolic operations in the console " "(e.g. integrals, derivatives, vector calculus, " "etc) and get the outputs in a beautifully " - "printed style.")) + "printed style (it requires the Sympy module).")) sympy_label.setWordWrap(True) sympy_box = newcb(_("Use symbolic math"), "symbolic_math", tip=_("This option loads the Sympy library to work " @@ -405,15 +523,7 @@ sympy_layout.addWidget(sympy_label) sympy_layout.addWidget(sympy_box) sympy_group.setLayout(sympy_layout) - - sympy_present = programs.is_module_installed("sympy") - if not sympy_present: - self.set_option("symbolic_math", False) - sympy_box.setEnabled(False) - sympy_tip = _("This feature requires the Sympy library.\n" - "It seems you don't have it installed.") - sympy_box.setToolTip(sympy_tip) - + # Prompts group prompts_group = QGroupBox(_("Prompts")) prompts_label = QLabel(_("Modify how Input and Output prompts are " @@ -434,14 +544,18 @@ prompts_layout = QVBoxLayout() prompts_layout.addWidget(prompts_label) - prompts_layout.addWidget(in_prompt_edit) - prompts_layout.addWidget(out_prompt_edit) + prompts_g_layout = QGridLayout() + prompts_g_layout.addWidget(in_prompt_edit.label, 0, 0) + prompts_g_layout.addWidget(in_prompt_edit.textbox, 0, 1) + prompts_g_layout.addWidget(out_prompt_edit.label, 1, 0) + prompts_g_layout.addWidget(out_prompt_edit.textbox, 1, 1) + prompts_layout.addLayout(prompts_g_layout) prompts_group.setLayout(prompts_layout) # --- Tabs organization --- tabs = QTabWidget() - tabs.addTab(self.create_tab(font_group, interface_group, bg_group, - source_code_group), _("Display")) + tabs.addTab(self.create_tab(interface_group, comp_group, + bg_group, source_code_group), _("Display")) tabs.addTab(self.create_tab(pylab_group, backend_group, inline_group), _("Graphics")) tabs.addTab(self.create_tab(run_lines_group, run_file_group), @@ -454,143 +568,46 @@ self.setLayout(vlayout) -class KernelConnectionDialog(QDialog): - """Dialog to connect to existing kernels (either local or remote)""" - - def __init__(self, parent=None): - super(KernelConnectionDialog, self).__init__(parent) - self.setWindowTitle(_('Connect to an existing kernel')) - - main_label = QLabel(_("Please enter the connection info of the kernel " - "you want to connect to. For that you can " - "either select its JSON connection file using " - "the Browse button, or write directly " - "its id, in case it's a local kernel (for " - "example kernel-3764.json or just " - "3764).")) - main_label.setWordWrap(True) - main_label.setAlignment(Qt.AlignJustify) - - # connection file - cf_label = QLabel(_('Connection info:')) - self.cf = QLineEdit() - self.cf.setPlaceholderText(_('Path to connection file or kernel id')) - self.cf.setMinimumWidth(250) - cf_open_btn = QPushButton(_('Browse')) - self.connect(cf_open_btn, SIGNAL('clicked()'), - self.select_connection_file) - - cf_layout = QHBoxLayout() - cf_layout.addWidget(cf_label) - cf_layout.addWidget(self.cf) - cf_layout.addWidget(cf_open_btn) - - # remote kernel checkbox - self.rm_cb = QCheckBox(_('This is a remote kernel')) - - # ssh connection - self.hn = QLineEdit() - self.hn.setPlaceholderText(_('username@hostname:port')) - - self.kf = QLineEdit() - self.kf.setPlaceholderText(_('Path to ssh key file')) - kf_open_btn = QPushButton(_('Browse')) - self.connect(kf_open_btn, SIGNAL('clicked()'), self.select_ssh_key) - - kf_layout = QHBoxLayout() - kf_layout.addWidget(self.kf) - kf_layout.addWidget(kf_open_btn) - - self.pw = QLineEdit() - self.pw.setPlaceholderText(_('Password or ssh key passphrase')) - self.pw.setEchoMode(QLineEdit.Password) - - ssh_form = QFormLayout() - ssh_form.addRow(_('Host name'), self.hn) - ssh_form.addRow(_('Ssh key'), kf_layout) - ssh_form.addRow(_('Password'), self.pw) - - # Ok and Cancel buttons - accept_btns = QDialogButtonBox( - QDialogButtonBox.Ok | QDialogButtonBox.Cancel, - Qt.Horizontal, self) - - self.connect(accept_btns, SIGNAL('accepted()'), self.accept) - self.connect(accept_btns, SIGNAL('rejected()'), self.reject) - - # Dialog layout - layout = QVBoxLayout(self) - layout.addWidget(main_label) - layout.addLayout(cf_layout) - layout.addWidget(self.rm_cb) - layout.addLayout(ssh_form) - layout.addWidget(accept_btns) - - # remote kernel checkbox enables the ssh_connection_form - def ssh_set_enabled(state): - for wid in [self.hn, self.kf, kf_open_btn, self.pw]: - wid.setEnabled(state) - for i in range(ssh_form.rowCount()): - ssh_form.itemAt(2 * i).widget().setEnabled(state) - - ssh_set_enabled(self.rm_cb.checkState()) - self.connect(self.rm_cb, SIGNAL('stateChanged(int)'), ssh_set_enabled) - - def select_connection_file(self): - cf = getopenfilename(self, _('Open IPython connection file'), - osp.join(get_ipython_dir(), 'profile_default', 'security'), - '*.json;;*.*')[0] - self.cf.setText(cf) - - def select_ssh_key(self): - kf = getopenfilename(self, _('Select ssh key'), - get_ipython_dir(), '*.pem;;*.*')[0] - self.kf.setText(kf) - - @staticmethod - def get_connection_parameters(parent=None): - dialog = KernelConnectionDialog(parent) - result = dialog.exec_() - is_remote = bool(dialog.rm_cb.checkState()) - accepted = result == QDialog.Accepted - if is_remote: - falsy_to_none = lambda arg: arg if arg else None - return (dialog.cf.text(), # connection file - falsy_to_none(dialog.hn.text()), # host name - falsy_to_none(dialog.kf.text()), # ssh key file - falsy_to_none(dialog.pw.text()), # ssh password - accepted) # ok - else: - return (dialog.cf.text(), None, None, None, accepted) - - +#------------------------------------------------------------------------------ +# Plugin widget +#------------------------------------------------------------------------------ class IPythonConsole(SpyderPluginWidget): """ IPython Console plugin - This is a widget with tabs where each one is an IPythonClient + This is a widget with tabs where each one is a ClientWidget """ CONF_SECTION = 'ipython_console' CONFIGWIDGET_CLASS = IPythonConsoleConfigPage DISABLE_ACTIONS_WHEN_HIDDEN = False + + # Signals + focus_changed = Signal() + edit_goto = Signal((str, int, str), (str, int, str, bool)) def __init__(self, parent): - SpyderPluginWidget.__init__(self, parent) + if PYQT5: + SpyderPluginWidget.__init__(self, parent, main = parent) + else: + SpyderPluginWidget.__init__(self, parent) self.tabwidget = None self.menu_actions = None self.extconsole = None # External console plugin - self.inspector = None # Object inspector plugin + self.help = None # Help plugin self.historylog = None # History log plugin self.variableexplorer = None # Variable explorer plugin + self.editor = None # Editor plugin self.master_clients = 0 self.clients = [] - + self.mainwindow_close = False + self.create_new_client_if_empty = True + # Initialize plugin self.initialize_plugin() - + layout = QVBoxLayout() self.tabwidget = Tabs(self, self.menu_actions) if hasattr(self.tabwidget, 'setDocumentMode')\ @@ -599,10 +616,8 @@ # a crash when the console is detached from the main window # Fixes Issue 561 self.tabwidget.setDocumentMode(True) - self.connect(self.tabwidget, SIGNAL('currentChanged(int)'), - self.refresh_plugin) - self.connect(self.tabwidget, SIGNAL('move_data(int,int)'), - self.move_tab) + self.tabwidget.currentChanged.connect(self.refresh_plugin) + self.tabwidget.move_data.connect(self.move_tab) self.tabwidget.set_close_function(self.close_client) @@ -619,31 +634,37 @@ # Find/replace widget self.find_widget = FindReplace(self) self.find_widget.hide() - self.register_widget_shortcuts("Editor", self.find_widget) + self.register_widget_shortcuts(self.find_widget) layout.addWidget(self.find_widget) - + self.setLayout(layout) - + # Accepting drops self.setAcceptDrops(True) - + #------ SpyderPluginMixin API --------------------------------------------- def on_first_registration(self): """Action to be performed on first plugin registration""" self.main.tabify_plugins(self.main.extconsole, self) + def update_font(self): + """Update font from Preferences""" + font = self.get_plugin_font() + for client in self.clients: + client.set_font(font) + def apply_plugin_settings(self, options): """Apply configuration file's plugin settings""" font_n = 'plugin_font' font_o = self.get_plugin_font() - inspector_n = 'connect_to_oi' - inspector_o = CONF.get('inspector', 'connect/ipython_console') + help_n = 'connect_to_oi' + help_o = CONF.get('help', 'connect/ipython_console') for client in self.clients: control = client.get_control() if font_n in options: client.set_font(font_o) - if inspector_n in options and control is not None: - control.set_inspector_enabled(inspector_o) + if help_n in options and control is not None: + control.set_help_enabled(help_o) def toggle_view(self, checked): """Toggle view""" @@ -666,7 +687,7 @@ def get_plugin_icon(self): """Return widget icon""" - return get_icon('ipython_console.png') + return ima.icon('ipython_console') def get_focus_widget(self): """ @@ -679,10 +700,12 @@ def closing_plugin(self, cancelable=False): """Perform actions before parent main window is closed""" + self.mainwindow_close = True for client in self.clients: + client.shutdown() client.close() return True - + def refresh_plugin(self): """Refresh tabwidget""" client = None @@ -692,34 +715,33 @@ control = client.get_control() control.setFocus() widgets = client.get_toolbar_buttons()+[5] - - # Change extconsole tab to the client's kernel widget - idx = self.extconsole.get_shell_index_from_id( - client.kernel_widget_id) - if idx is not None: - self.extconsole.tabwidget.setCurrentIndex(idx) else: control = None widgets = [] self.find_widget.set_editor(control) self.tabwidget.set_corner_widgets({Qt.TopRightCorner: widgets}) + if client: + sw = client.shellwidget + self.variableexplorer.set_shellwidget_from_id(id(sw)) + self.help.set_shell(sw) self.main.last_console_plugin_focus_was_python = False - self.emit(SIGNAL('update_plugin_title()')) + self.update_plugin_title.emit() def get_plugin_actions(self): """Return a list of actions related to plugin""" ctrl = "Cmd" if sys.platform == "darwin" else "Ctrl" main_create_client_action = create_action(self, _("Open an &IPython console"), - None, 'ipython_console.png', + None, ima.icon('ipython_console'), triggered=self.create_new_client, tip=_("Use %s+T when the console is selected " "to open a new one") % ctrl) create_client_action = create_action(self, _("Open a new console"), - QKeySequence("Ctrl+T"), 'ipython_console.png', - triggered=self.create_new_client) - create_client_action.setShortcutContext(Qt.WidgetWithChildrenShortcut) + QKeySequence("Ctrl+T"), + ima.icon('ipython_console'), + triggered=self.create_new_client, + context=Qt.WidgetWithChildrenShortcut) connect_to_kernel_action = create_action(self, _("Connect to an existing kernel"), None, None, @@ -741,30 +763,26 @@ self.main.add_dockwidget(self) self.extconsole = self.main.extconsole - self.inspector = self.main.inspector + self.help = self.main.help self.historylog = self.main.historylog self.variableexplorer = self.main.variableexplorer - - self.connect(self, SIGNAL('focus_changed()'), - self.main.plugin_focus_changed) - - if self.main.editor is not None: - self.connect(self, SIGNAL("edit_goto(QString,int,QString)"), - self.main.editor.load) - self.connect(self.main.editor, - SIGNAL('run_in_current_ipyclient(QString,QString,QString,bool)'), - self.run_script_in_current_client) + self.editor = self.main.editor + + self.focus_changed.connect(self.main.plugin_focus_changed) + self.edit_goto.connect(self.editor.load) + self.edit_goto[str, int, str, bool].connect( + lambda fname, lineno, word, processevents: + self.editor.load(fname, lineno, word, + processevents=processevents)) + self.editor.run_in_current_ipyclient.connect( + self.run_script_in_current_client) + self.main.workingdirectory.set_current_console_wd.connect( + self.set_current_client_working_directory) #------ Public API (for clients) ------------------------------------------ def get_clients(self): """Return clients list""" - return [cl for cl in self.clients if isinstance(cl, IPythonClient)] - -# def get_kernels(self): -# """Return IPython kernel widgets list""" -# return [sw for sw in self.shellwidgets -# if isinstance(sw, IPythonKernel)] -# + return [cl for cl in self.clients if isinstance(cl, ClientWidget)] def get_focus_client(self): """Return current client with focus, if any""" @@ -779,19 +797,28 @@ if client is not None: return client - def run_script_in_current_client(self, filename, wdir, args, debug): + def get_current_shellwidget(self): + """Return the shellwidget of the current client""" + client = self.get_current_client() + if client is not None: + return client.shellwidget + + def run_script_in_current_client(self, filename, wdir, args, debug, + post_mortem): """Run script in current client, if any""" norm = lambda text: remove_backslashes(to_text_string(text)) client = self.get_current_client() if client is not None: # Internal kernels, use runfile - if client.kernel_widget_id is not None: + if client.get_kernel() is not None: line = "%s('%s'" % ('debugfile' if debug else 'runfile', norm(filename)) if args: line += ", args='%s'" % norm(args) if wdir: line += ", wdir='%s'" % norm(wdir) + if post_mortem: + line += ", post_mortem=True" line += ")" else: # External kernels, use %run line = "%run " @@ -800,7 +827,7 @@ line += "\"%s\"" % to_text_string(filename) if args: line += " %s" % norm(args) - self.execute_python_code(line) + self.execute_code(line) self.visibility_changed(True) self.raise_() else: @@ -810,122 +837,253 @@ "

    Please open a new one and try again." ) % osp.basename(filename), QMessageBox.Ok) - def execute_python_code(self, lines): - client = self.get_current_client() - if client is not None: - client.shellwidget.execute(to_text_string(lines)) + def set_current_client_working_directory(self, directory): + """Set current client working directory.""" + shellwidget = self.get_current_shellwidget() + if shellwidget is not None: + directory = encoding.to_unicode_from_fs(directory) + shellwidget.set_cwd(directory) + + def execute_code(self, lines): + sw = self.get_current_shellwidget() + if sw is not None: + sw.execute(to_text_string(lines)) self.activateWindow() - client.get_control().setFocus() + self.get_current_client().get_control().setFocus() def write_to_stdin(self, line): - client = self.get_current_client() - if client is not None: - client.shellwidget.write_to_stdin(line) + sw = self.get_current_shellwidget() + if sw is not None: + sw.write_to_stdin(line) + @Slot() def create_new_client(self, give_focus=True): """Create a new client""" self.master_clients += 1 name = "%d/A" % self.master_clients - client = IPythonClient(self, name=name, history_filename='history.py', - menu_actions=self.menu_actions) + client = ClientWidget(self, name=name, + history_filename='history.py', + config_options=self.config_options(), + additional_options=self.additional_options(), + interpreter_versions=self.interpreter_versions(), + connection_file=self._new_connection_file(), + menu_actions=self.menu_actions) self.add_tab(client, name=client.get_name()) - self.main.extconsole.start_ipykernel(client, give_focus=give_focus) - def register_client(self, client, restart=False, give_focus=True): - """Register new client""" + # Check if ipykernel is present in the external interpreter. + # Else we won't be able to create a client + if not CONF.get('main_interpreter', 'default'): + pyexec = CONF.get('main_interpreter', 'executable') + ipykernel_present = programs.is_module_installed('ipykernel', + interpreter=pyexec) + if not ipykernel_present: + client.show_kernel_error(_("Your Python environment or " + "installation doesn't " + "have the ipykernel module " + "installed on it. Without this module is " + "not possible for Spyder to create a " + "console for you.

    " + "You can install ipykernel by " + "running in a terminal:

    " + "pip install ipykernel

    " + "or

    " + "conda install ipykernel")) + return + self.connect_client_to_kernel(client) - client.show_shellwidget(give_focus=give_focus) - + self.register_client(client) + + @Slot() + def create_client_for_kernel(self): + """Create a client connected to an existing kernel""" + connect_output = KernelConnectionDialog.get_connection_parameters(self) + (connection_file, hostname, sshkey, password, ok) = connect_output + if not ok: + return + else: + self._create_client_for_kernel(connection_file, hostname, sshkey, + password) + + def connect_client_to_kernel(self, client): + """Connect a client to its kernel""" + connection_file = client.connection_file + km, kc = self.create_kernel_manager_and_kernel_client(connection_file) + + kc.started_channels.connect(lambda c=client: self.process_started(c)) + kc.stopped_channels.connect(lambda c=client: self.process_finished(c)) + kc.start_channels(shell=True, iopub=True) + + shellwidget = client.shellwidget + shellwidget.kernel_manager = km + shellwidget.kernel_client = kc + + def set_editor(self): + """Set the editor used by the %edit magic""" + python = sys.executable + if DEV: + spyder_start_directory = get_module_path('spyder') + bootstrap_script = osp.join(osp.dirname(spyder_start_directory), + 'bootstrap.py') + editor = u'{0} {1} --'.format(python, bootstrap_script) + else: + import1 = "import sys" + import2 = "from spyder.app.start import send_args_to_spyder" + code = "send_args_to_spyder([sys.argv[-1]])" + editor = u"{0} -c '{1}; {2}; {3}'".format(python, + import1, + import2, + code) + return to_text_string(editor) + + def config_options(self): + """ + Generate a Trailets Config instance for shell widgets using our + config system + + This lets us create each widget with its own config + """ + # ---- Jupyter config ---- + try: + full_cfg = load_pyconfig_files(['jupyter_qtconsole_config.py'], + jupyter_config_dir()) + + # From the full config we only select the JupyterWidget section + # because the others have no effect here. + cfg = Config({'JupyterWidget': full_cfg.JupyterWidget}) + except: + cfg = Config() + + # ---- Spyder config ---- + spy_cfg = Config() + + # Make the pager widget a rich one (i.e a QTextEdit) + spy_cfg.JupyterWidget.kind = 'rich' + + # Gui completion widget + completion_type_o = self.get_option('completion_type') + completions = {0: "droplist", 1: "ncurses", 2: "plain"} + spy_cfg.JupyterWidget.gui_completion = completions[completion_type_o] + + # Pager + pager_o = self.get_option('use_pager') + if pager_o: + spy_cfg.JupyterWidget.paging = 'inside' + else: + spy_cfg.JupyterWidget.paging = 'none' + + # Calltips + calltips_o = self.get_option('show_calltips') + spy_cfg.JupyterWidget.enable_calltips = calltips_o + + # Buffer size + buffer_size_o = self.get_option('buffer_size') + spy_cfg.JupyterWidget.buffer_size = buffer_size_o + + # Prompts + in_prompt_o = self.get_option('in_prompt') + out_prompt_o = self.get_option('out_prompt') + if in_prompt_o: + spy_cfg.JupyterWidget.in_prompt = in_prompt_o + if out_prompt_o: + spy_cfg.JupyterWidget.out_prompt = out_prompt_o + + # Editor for %edit + if CONF.get('main', 'single_instance'): + spy_cfg.JupyterWidget.editor = self.set_editor() + + # Merge QtConsole and Spyder configs. Spyder prefs will have + # prevalence over QtConsole ones + cfg._merge(spy_cfg) + return cfg + + def interpreter_versions(self): + """Python and IPython versions used by clients""" + if CONF.get('main_interpreter', 'default'): + from IPython.core import release + versions = dict( + python_version = sys.version.split("\n")[0].strip(), + ipython_version = release.version + ) + else: + import subprocess + versions = {} + pyexec = CONF.get('main_interpreter', 'executable') + py_cmd = "%s -c 'import sys; print(sys.version.split(\"\\n\")[0])'" % \ + pyexec + ipy_cmd = "%s -c 'import IPython.core.release as r; print(r.version)'" \ + % pyexec + for cmd in [py_cmd, ipy_cmd]: + try: + proc = programs.run_shell_command(cmd) + output, _err = proc.communicate() + except subprocess.CalledProcessError: + output = '' + output = output.decode().split('\n')[0].strip() + if 'IPython' in cmd: + versions['ipython_version'] = output + else: + versions['python_version'] = output + + return versions + + def additional_options(self): + """ + Additional options for shell widgets that are not defined + in JupyterWidget config options + """ + options = dict( + pylab = self.get_option('pylab'), + autoload_pylab = self.get_option('pylab/autoload'), + sympy = self.get_option('symbolic_math'), + light_color = self.get_option('light_color'), + show_banner = self.get_option('show_banner') + ) + + return options + + def register_client(self, client, give_focus=True): + """Register new client""" + client.configure_shellwidget(give_focus=give_focus) + # Local vars shellwidget = client.shellwidget control = shellwidget._control page_control = shellwidget._page_control # Create new clients with Ctrl+T shortcut - self.connect(shellwidget, SIGNAL('new_ipyclient()'), - self.create_new_client) - - # Handle kernel interrupts - extconsoles = self.extconsole.shellwidgets - kernel_widget = None - if extconsoles: - if extconsoles[-1].connection_file == client.connection_file: - kernel_widget = extconsoles[-1] - if restart: - shellwidget.custom_interrupt_requested.disconnect() - shellwidget.custom_interrupt_requested.connect( - kernel_widget.keyboard_interrupt) - if kernel_widget is None: - shellwidget.custom_interrupt_requested.connect( - client.interrupt_message) - - # Connect to our variable explorer - if kernel_widget is not None and self.variableexplorer is not None: - nsb = self.variableexplorer.currentWidget() - # When the autorefresh button is active, our kernels - # start to consume more and more CPU during time - # Fix Issue 1450 - # ---------------- - # When autorefresh is off by default we need the next - # line so that kernels don't start to consume CPU - # Fix Issue 1595 - nsb.auto_refresh_button.setChecked(True) - nsb.auto_refresh_button.setChecked(False) - nsb.auto_refresh_button.setEnabled(False) - nsb.set_ipyclient(client) - client.set_namespacebrowser(nsb) - - # If we are restarting the kernel we need to rename - # the client tab and do no more from here on - if restart: - self.rename_client_tab(client) - return - + shellwidget.new_client.connect(self.create_new_client) + # For tracebacks - self.connect(control, SIGNAL("go_to_error(QString)"), self.go_to_error) - - # Handle kernel restarts asked by the user - if kernel_widget is not None: - shellwidget.custom_restart_requested.connect( - lambda cl=client: self.restart_kernel(client)) - else: - shellwidget.custom_restart_requested.connect(client.restart_message) - - # Print a message if kernel dies unexpectedly - shellwidget.custom_restart_kernel_died.connect( - lambda t: client.if_kernel_dies(t)) - - # Connect text widget to our inspector - if kernel_widget is not None and self.inspector is not None: - control.set_inspector(self.inspector) - control.set_inspector_enabled(CONF.get('inspector', - 'connect/ipython_console')) + control.go_to_error.connect(self.go_to_error) + + shellwidget.sig_pdb_step.connect( + lambda fname, lineno, shellwidget=shellwidget: + self.pdb_has_stopped(fname, lineno, shellwidget)) + + # Connect text widget to Help + if self.help is not None: + control.set_help(self.help) + control.set_help_enabled(CONF.get('help', 'connect/ipython_console')) # Connect client to our history log if self.historylog is not None: self.historylog.add_history(client.history_filename) - self.connect(client, SIGNAL('append_to_history(QString,QString)'), - self.historylog.append_to_history) + client.append_to_history.connect(self.historylog.append_to_history) # Set font for client client.set_font( self.get_plugin_font() ) # Connect focus signal to client's control widget - self.connect(control, SIGNAL('focus_changed()'), - lambda: self.emit(SIGNAL('focus_changed()'))) + control.focus_changed.connect(lambda: self.focus_changed.emit()) # Update the find widget if focus changes between control and # page_control self.find_widget.set_editor(control) if page_control: - self.connect(page_control, SIGNAL('focus_changed()'), - lambda: self.emit(SIGNAL('focus_changed()'))) - self.connect(control, SIGNAL('visibility_changed(bool)'), - self.refresh_plugin) - self.connect(page_control, SIGNAL('visibility_changed(bool)'), - self.refresh_plugin) - self.connect(page_control, SIGNAL('show_find_widget()'), - self.find_widget.show) + page_control.focus_changed.connect(lambda: self.focus_changed.emit()) + control.visibility_changed.connect(self.refresh_plugin) + page_control.visibility_changed.connect(self.refresh_plugin) + page_control.show_find_widget.connect(self.find_widget.show) def close_client(self, index=None, client=None, force=False): """Close client tab from index or widget (or close current tab)""" @@ -940,39 +1098,39 @@ # Check if related clients or kernels are opened # and eventually ask before closing them - if not force and isinstance(client, IPythonClient): - kernel_index = self.extconsole.get_shell_index_from_id( - client.kernel_widget_id) + if not self.mainwindow_close and not force: close_all = True - if len(self.get_related_clients(client)) > 0 and \ - self.get_option('ask_before_closing'): - ans = QMessageBox.question(self, self.get_plugin_title(), - _("Do you want to close all other consoles connected " - "to the same kernel as this one?"), - QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel) - if ans == QMessageBox.Cancel: + if self.get_option('ask_before_closing'): + close = QMessageBox.question(self, self.get_plugin_title(), + _("Do you want to close this console?"), + QMessageBox.Yes | QMessageBox.No) + if close == QMessageBox.No: return - close_all = ans == QMessageBox.Yes - if close_all: - if kernel_index is not None: - self.extconsole.close_console(index=kernel_index, - from_ipyclient=True) + if len(self.get_related_clients(client)) > 0: + close_all = QMessageBox.question(self, self.get_plugin_title(), + _("Do you want to close all other consoles connected " + "to the same kernel as this one?"), + QMessageBox.Yes | QMessageBox.No) + client.shutdown() + if close_all == QMessageBox.Yes: self.close_related_clients(client) client.close() - + # Note: client index may have changed after closing related widgets self.tabwidget.removeTab(self.tabwidget.indexOf(client)) self.clients.remove(client) - self.emit(SIGNAL('update_plugin_title()')) + if not self.tabwidget.count() and self.create_new_client_if_empty: + self.create_new_client() + self.update_plugin_title.emit() def get_client_index_from_id(self, client_id): """Return client index from id""" for index, client in enumerate(self.clients): if id(client) == client_id: return index - + def rename_client_tab(self, client): - """Add the pid of the kernel process to client tab""" + """Rename client's tab""" index = self.get_client_index_from_id(id(client)) self.tabwidget.setTabText(index, client.get_name()) @@ -993,158 +1151,163 @@ for cl in related_clients: self.close_client(client=cl, force=True) + def restart(self): + """ + Restart the console + + This is needed when we switch projects to update PYTHONPATH + and the selected interpreter + """ + self.master_clients = 0 + self.create_new_client_if_empty = False + for i in range(len(self.clients)): + client = self.clients[-1] + client.shutdown() + self.close_client(client=client, force=True) + self.create_new_client(give_focus=False) + self.create_new_client_if_empty = True + + def pdb_has_stopped(self, fname, lineno, shellwidget): + """Python debugger has just stopped at frame (fname, lineno)""" + # This is a unique form of the edit_goto signal that is intended to + # prevent keyboard input from accidentally entering the editor + # during repeated, rapid entry of debugging commands. + self.edit_goto[str, int, str, bool].emit(fname, lineno, '', False) + self.activateWindow() + shellwidget._control.setFocus() + #------ Public API (for kernels) ------------------------------------------ def ssh_tunnel(self, *args, **kwargs): - if sys.platform == 'win32': + if os.name == 'nt': return zmqtunnel.paramiko_tunnel(*args, **kwargs) else: return openssh_tunnel(self, *args, **kwargs) - def tunnel_to_kernel(self, ci, hostname, sshkey=None, password=None, timeout=10): - """tunnel connections to a kernel via ssh. remote ports are specified in - the connection info ci.""" + def tunnel_to_kernel(self, connection_info, hostname, sshkey=None, + password=None, timeout=10): + """ + Tunnel connections to a kernel via ssh. + + Remote ports are specified in the connection info ci. + """ lports = zmqtunnel.select_random_ports(4) - rports = ci['shell_port'], ci['iopub_port'], ci['stdin_port'], ci['hb_port'] - remote_ip = ci['ip'] + rports = (connection_info['shell_port'], connection_info['iopub_port'], + connection_info['stdin_port'], connection_info['hb_port']) + remote_ip = connection_info['ip'] for lp, rp in zip(lports, rports): - self.ssh_tunnel(lp, rp, hostname, remote_ip, sshkey, password, timeout) + self.ssh_tunnel(lp, rp, hostname, remote_ip, sshkey, password, + timeout) return tuple(lports) - def create_kernel_manager_and_client(self, connection_file=None, - hostname=None, sshkey=None, - password=None): - """Create kernel manager and client""" - cf = find_connection_file(connection_file) - kernel_manager = QtKernelManager(connection_file=cf, config=None) - kernel_client = kernel_manager.client() - kernel_client.load_connection_file() - if hostname is not None: - try: - newports = self.tunnel_to_kernel(dict(ip=kernel_client.ip, - shell_port=kernel_client.shell_port, - iopub_port=kernel_client.iopub_port, - stdin_port=kernel_client.stdin_port, - hb_port=kernel_client.hb_port), - hostname, sshkey, password) - (kernel_client.shell_port, kernel_client.iopub_port, - kernel_client.stdin_port, kernel_client.hb_port) = newports - except Exception as e: - QMessageBox.critical(self, _('Connection error'), - _("Could not open ssh tunnel. The " - "error was:\n\n") + to_text_string(e)) - return None, None - kernel_client.start_channels() - # To rely on kernel's heartbeat to know when a kernel has died - kernel_client.hb_channel.unpause() - return kernel_manager, kernel_client + def create_kernel_spec(self): + """Create a kernel spec for our own kernels""" + # Paths that we need to add to PYTHONPATH: + # 1. sc_path: Path to our sitecustomize + # 2. spy_path: Path to our main module, so we can use our config + # system to configure kernels started by exterrnal interpreters + # 3. spy_pythonpath: Paths saved by our users with our PYTHONPATH + # manager + spy_path = get_module_source_path('spyder') + sc_path = osp.join(spy_path, 'utils', 'site') + spy_pythonpath = self.main.get_spyder_pythonpath() + + default_interpreter = CONF.get('main_interpreter', 'default') + if default_interpreter: + pathlist = [sc_path] + spy_pythonpath + else: + pathlist = [sc_path, spy_path] + spy_pythonpath + pypath = add_pathlist_to_PYTHONPATH([], pathlist, ipyconsole=True, + drop_env=(not default_interpreter)) + + # Python interpreter used to start kernels + if default_interpreter: + pyexec = get_python_executable() + else: + # Avoid IPython adding the virtualenv on which Spyder is running + # to the kernel sys.path + os.environ.pop('VIRTUAL_ENV', None) + pyexec = CONF.get('main_interpreter', 'executable') + + # Fixes Issue #3427 + if os.name == 'nt': + dir_pyexec = osp.dirname(pyexec) + pyexec_w = osp.join(dir_pyexec, 'pythonw.exe') + if osp.isfile(pyexec_w): + pyexec = pyexec_w + + # Command used to start kernels + utils_path = osp.join(spy_path, 'utils', 'ipython') + kernel_cmd = [ + pyexec, + osp.join("%s" % utils_path, "start_kernel.py"), + '-f', + '{connection_file}' + ] + + # Environment variables that we need to pass to our sitecustomize + umr_namelist = CONF.get('main_interpreter', 'umr/namelist') + + if PY2: + original_list = umr_namelist[:] + for umr_n in umr_namelist: + try: + umr_n.encode('utf-8') + except UnicodeDecodeError: + umr_namelist.remove(umr_n) + if original_list != umr_namelist: + CONF.set('main_interpreter', 'umr/namelist', umr_namelist) + + env_vars = { + 'IPYTHON_KERNEL': 'True', + 'EXTERNAL_INTERPRETER': not default_interpreter, + 'UMR_ENABLED': CONF.get('main_interpreter', 'umr/enabled'), + 'UMR_VERBOSE': CONF.get('main_interpreter', 'umr/verbose'), + 'UMR_NAMELIST': ','.join(umr_namelist) + } + + # Add our PYTHONPATH to env_vars + env_vars.update(pypath) + + # Making all env_vars strings + for k,v in iteritems(env_vars): + if PY2: + uv = to_text_string(v) + env_vars[k] = to_binary_string(uv, encoding='utf-8') + else: + env_vars[k] = to_text_string(v) - def connect_client_to_kernel(self, client): - """ - Connect a client to its kernel - """ - km, kc = self.create_kernel_manager_and_client(client.connection_file, - client.hostname, - client.sshkey, - client.password) - if km is not None: - widget = client.shellwidget - widget.kernel_manager = km - widget.kernel_client = kc + # Dict for our kernel spec + kernel_dict = { + 'argv': kernel_cmd, + 'display_name': 'Spyder', + 'language': 'python', + 'env': env_vars + } - def create_client_for_kernel(self): - """Create a client connected to an existing kernel""" - (cf, hostname, - kf, pw, ok) = KernelConnectionDialog.get_connection_parameters(self) - if not ok: - return - else: - self._create_client_for_kernel(cf, hostname, kf, pw) + return KernelSpec(resource_dir='', **kernel_dict) - def _create_client_for_kernel(self, cf, hostname, kf, pw): - # Verifying if the connection file exists - cf = osp.basename(cf) - try: - find_connection_file(cf) - except (IOError, UnboundLocalError): - QMessageBox.critical(self, _('IPython'), - _("Unable to connect to IPython %s") % cf) - return - - # Getting the master name that corresponds to the client - # (i.e. the i in i/A) - master_name = None - slave_ord = ord('A') - 1 - for cl in self.get_clients(): - if cf in cl.connection_file: - cf = cl.connection_file - if master_name is None: - master_name = cl.name.split('/')[0] - new_slave_ord = ord(cl.name.split('/')[1]) - if new_slave_ord > slave_ord: - slave_ord = new_slave_ord - - # If we couldn't find a client with the same connection file, - # it means this is a new master client - if master_name is None: - self.master_clients += 1 - master_name = to_text_string(self.master_clients) - - # Set full client name - name = master_name + '/' + chr(slave_ord + 1) - - # Getting kernel_widget_id from the currently open kernels. - kernel_widget_id = None - for sw in self.extconsole.shellwidgets: - if sw.connection_file == cf.split('/')[-1]: - kernel_widget_id = id(sw) + def create_kernel_manager_and_kernel_client(self, connection_file): + """Create kernel manager and client""" + # Kernel manager + kernel_manager = QtKernelManager(connection_file=connection_file, + config=None, autorestart=True) + kernel_manager._kernel_spec = self.create_kernel_spec() + kernel_manager.start_kernel() - # Creating the client - client = IPythonClient(self, name=name, history_filename='history.py', - connection_file=cf, - kernel_widget_id=kernel_widget_id, - menu_actions=self.menu_actions, - hostname=hostname, sshkey=kf, password=pw) - - # Adding the tab - self.add_tab(client, name=client.get_name()) - - # Connecting kernel and client - self.register_client(client) + # Kernel client + kernel_client = kernel_manager.client() - def restart_kernel(self, client): - """ - Create a new kernel and connect it to `client` if the user asks for it - """ - # Took this bit of code (until if result == ) from the IPython project - # (qt/frontend_widget.py - restart_kernel). - # Licensed under the BSD license - message = _('Are you sure you want to restart the kernel?') - buttons = QMessageBox.Yes | QMessageBox.No - result = QMessageBox.question(self, _('Restart kernel?'), - message, buttons) - if result == QMessageBox.Yes: - client.show_restart_animation() - # Close old kernel tab - idx = self.extconsole.get_shell_index_from_id(client.kernel_widget_id) - self.extconsole.close_console(index=idx, from_ipyclient=True) - - # Create a new one and connect it to the client - self.extconsole.start_ipykernel(client) - - def get_shellwidget_by_kernelwidget_id(self, kernel_id): - """Return the IPython widget associated to a kernel widget id""" - for cl in self.clients: - if cl.kernel_widget_id == kernel_id: - return cl.shellwidget - else: - raise ValueError("Unknown kernel widget ID %r" % kernel_id) + # Increase time to detect if a kernel is alive + # See Issue 3444 + kernel_client.hb_channel.time_to_dead = 6.0 + + return kernel_manager, kernel_client #------ Public API (for tabs) --------------------------------------------- def add_tab(self, widget, name): """Add tab""" self.clients.append(widget) - index = self.tabwidget.addTab(widget, get_icon('ipython_console.png'), - name) + index = self.tabwidget.addTab(widget, name) self.tabwidget.setCurrentIndex(index) if self.dockwidget and not self.ismaximized: self.dockwidget.setVisible(True) @@ -1158,7 +1321,7 @@ """ client = self.clients.pop(index_from) self.clients.insert(index_to, client) - self.emit(SIGNAL('update_plugin_title()')) + self.update_plugin_title.emit() #------ Public API (for help) --------------------------------------------- def go_to_error(self, text): @@ -1166,63 +1329,133 @@ match = get_error_match(to_text_string(text)) if match: fname, lnb = match.groups() - self.emit(SIGNAL("edit_goto(QString,int,QString)"), - osp.abspath(fname), int(lnb), '') - + self.edit_goto.emit(osp.abspath(fname), int(lnb), '') + + @Slot() def show_intro(self): """Show intro to IPython help""" from IPython.core.usage import interactive_usage - self.inspector.show_rich_text(interactive_usage) - + self.help.show_rich_text(interactive_usage) + + @Slot() def show_guiref(self): """Show qtconsole help""" - from IPython.core.usage import gui_reference - self.inspector.show_rich_text(gui_reference, collapse=True) - + from qtconsole.usage import gui_reference + self.help.show_rich_text(gui_reference, collapse=True) + + @Slot() def show_quickref(self): """Show IPython Cheat Sheet""" from IPython.core.usage import quick_reference - self.inspector.show_plain_text(quick_reference) + self.help.show_plain_text(quick_reference) + + #------ Private API ------------------------------------------------------- + def _new_connection_file(self): + """ + Generate a new connection file + + Taken from jupyter_client/console_app.py + Licensed under the BSD license + """ + # Check if jupyter_runtime_dir exists (Spyder addition) + if not osp.isdir(jupyter_runtime_dir()): + os.makedirs(jupyter_runtime_dir()) + cf = '' + while not cf: + ident = str(uuid.uuid4()).split('-')[-1] + cf = os.path.join(jupyter_runtime_dir(), 'kernel-%s.json' % ident) + cf = cf if not os.path.exists(cf) else '' + return cf + + def process_started(self, client): + if self.help is not None: + self.help.set_shell(client.shellwidget) + if self.variableexplorer is not None: + self.variableexplorer.add_shellwidget(client.shellwidget) + + def process_finished(self, client): + if self.variableexplorer is not None: + self.variableexplorer.remove_shellwidget(id(client.shellwidget)) + + def _create_client_for_kernel(self, connection_file, hostname, sshkey, + password): + # Verifying if the connection file exists + try: + connection_file = find_connection_file(osp.basename(connection_file)) + except (IOError, UnboundLocalError): + QMessageBox.critical(self, _('IPython'), + _("Unable to connect to " + "%s") % connection_file) + return + + # Getting the master name that corresponds to the client + # (i.e. the i in i/A) + master_name = None + external_kernel = False + slave_ord = ord('A') - 1 + kernel_manager = None + for cl in self.get_clients(): + if connection_file in cl.connection_file: + if cl.get_kernel() is not None: + kernel_manager = cl.get_kernel() + connection_file = cl.connection_file + if master_name is None: + master_name = cl.name.split('/')[0] + new_slave_ord = ord(cl.name.split('/')[1]) + if new_slave_ord > slave_ord: + slave_ord = new_slave_ord - #----Drag and drop - #TODO: try and reimplement this block - # (this is still the original code block copied from externalconsole.py) -# def dragEnterEvent(self, event): -# """Reimplement Qt method -# Inform Qt about the types of data that the widget accepts""" -# source = event.mimeData() -# if source.hasUrls(): -# if mimedata2url(source): -# pathlist = mimedata2url(source) -# shellwidget = self.tabwidget.currentWidget() -# if all([is_python_script(unicode(qstr)) for qstr in pathlist]): -# event.acceptProposedAction() -# elif shellwidget is None or not shellwidget.is_running(): -# event.ignore() -# else: -# event.acceptProposedAction() -# else: -# event.ignore() -# elif source.hasText(): -# event.acceptProposedAction() -# -# def dropEvent(self, event): -# """Reimplement Qt method -# Unpack dropped data and handle it""" -# source = event.mimeData() -# shellwidget = self.tabwidget.currentWidget() -# if source.hasText(): -# qstr = source.text() -# if is_python_script(unicode(qstr)): -# self.start(qstr) -# elif shellwidget: -# shellwidget.shell.insert_text(qstr) -# elif source.hasUrls(): -# pathlist = mimedata2url(source) -# if all([is_python_script(unicode(qstr)) for qstr in pathlist]): -# for fname in pathlist: -# self.start(fname) -# elif shellwidget: -# shellwidget.shell.drop_pathlist(pathlist) -# event.acceptProposedAction() + # If we couldn't find a client with the same connection file, + # it means this is a new master client + if master_name is None: + self.master_clients += 1 + master_name = to_text_string(self.master_clients) + external_kernel = True + + # Set full client name + name = master_name + '/' + chr(slave_ord + 1) + + # Creating the client + client = ClientWidget(self, name=name, + history_filename='history.py', + config_options=self.config_options(), + additional_options=self.additional_options(), + interpreter_versions=self.interpreter_versions(), + connection_file=connection_file, + menu_actions=self.menu_actions, + hostname=hostname, + external_kernel=external_kernel, + slave=True) + # Create kernel client + kernel_client = QtKernelClient(connection_file=connection_file) + kernel_client.load_connection_file() + if hostname is not None: + try: + connection_info = dict(ip = kernel_client.ip, + shell_port = kernel_client.shell_port, + iopub_port = kernel_client.iopub_port, + stdin_port = kernel_client.stdin_port, + hb_port = kernel_client.hb_port) + newports = self.tunnel_to_kernel(connection_info, hostname, + sshkey, password) + (kernel_client.shell_port, + kernel_client.iopub_port, + kernel_client.stdin_port, + kernel_client.hb_port) = newports + except Exception as e: + QMessageBox.critical(self, _('Connection error'), + _("Could not open ssh tunnel. The " + "error was:\n\n") + to_text_string(e)) + return + kernel_client.start_channels() + + # Assign kernel manager and client to shellwidget + client.shellwidget.kernel_client = kernel_client + client.shellwidget.kernel_manager = kernel_manager + + # Adding a new tab for the client + self.add_tab(client, name=client.get_name()) + + # Register client + self.register_client(client) diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/plugins/layoutdialog.py spyder-3.0.2+dfsg1/spyder/plugins/layoutdialog.py --- spyder-2.3.8+dfsg1/spyder/plugins/layoutdialog.py 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/plugins/layoutdialog.py 2016-10-25 02:05:23.000000000 +0200 @@ -0,0 +1,344 @@ +# -*- coding: utf-8 -*- +# +# Copyright © Spyder Project Contributors +# Licensed under the terms of the MIT License +# (see spyder/__init__.py for details) + +"""Layout dialogs""" + +# Standard library imports +import sys + +# Third party imports +from qtpy.QtCore import QAbstractTableModel, QModelIndex, QSize, Qt +from qtpy.compat import from_qvariant, to_qvariant +from qtpy.QtWidgets import (QAbstractItemView, QComboBox, QDialog, + QDialogButtonBox, QGroupBox, QHBoxLayout, + QPushButton, QTableView, QVBoxLayout) + +# Local imports +from spyder.config.base import _ +from spyder.py3compat import to_text_string + + +class LayoutModel(QAbstractTableModel): + """ """ + def __init__(self, parent, order, active): + super(LayoutModel, self).__init__(parent) + + # variables + self._parent = parent + self.order = order + self.active = active + self._rows = [] + self.set_data(order, active) + + def set_data(self, order, active): + """ """ + self._rows = [] + self.order = order + self.active = active + for name in order: + if name in active: + row = [name, True] + else: + row = [name, False] + self._rows.append(row) + + def flags(self, index): + """Override Qt method""" + if not index.isValid(): + return Qt.ItemIsEnabled + column = index.column() + if column in [0]: + return Qt.ItemFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable | + Qt.ItemIsUserCheckable | Qt.ItemIsEditable) + else: + return Qt.ItemFlags(Qt.ItemIsEnabled) + + def data(self, index, role=Qt.DisplayRole): + """Override Qt method""" + if not index.isValid() or not 0 <= index.row() < len(self._rows): + return to_qvariant() + row = index.row() + column = index.column() + + name, state = self.row(row) + + if role == Qt.DisplayRole or role == Qt.EditRole: + if column == 0: + return to_qvariant(name) + elif role == Qt.CheckStateRole: + if column == 0: + if state: + return Qt.Checked + else: + return Qt.Unchecked + if column == 1: + return to_qvariant(state) + return to_qvariant() + + def setData(self, index, value, role): + """Override Qt method""" + row = index.row() + name, state = self.row(row) + + if role == Qt.CheckStateRole: + self.set_row(row, [name, not state]) + self._parent.setCurrentIndex(index) + self._parent.setFocus() + self.dataChanged.emit(index, index) + return True + elif role == Qt.EditRole: + self.set_row(row, [from_qvariant(value, to_text_string), state]) + self.dataChanged.emit(index, index) + return True + return True + + def rowCount(self, index=QModelIndex()): + """Override Qt method""" + return len(self._rows) + + def columnCount(self, index=QModelIndex()): + """Override Qt method""" + return 2 + + def row(self, rownum): + """ """ + if self._rows == []: + return [None, None] + else: + return self._rows[rownum] + + def set_row(self, rownum, value): + """ """ + self._rows[rownum] = value + + +class LayoutSaveDialog(QDialog): + """ """ + def __init__(self, parent, order): + super(LayoutSaveDialog, self).__init__(parent) + + # variables + self._parent = parent + + # widgets + self.combo_box = QComboBox(self) + self.combo_box.addItems(order) + self.combo_box.setEditable(True) + self.combo_box.clearEditText() + self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | + QDialogButtonBox.Cancel, + Qt.Horizontal, self) + self.button_ok = self.button_box.button(QDialogButtonBox.Ok) + self.button_cancel = self.button_box.button(QDialogButtonBox.Cancel) + + # widget setup + self.button_ok.setEnabled(False) + self.dialog_size = QSize(300, 100) + self.setWindowTitle('Save layout as') + self.setModal(True) + self.setMinimumSize(self.dialog_size) + self.setFixedSize(self.dialog_size) + + # layouts + self.layout = QVBoxLayout() + self.layout.addWidget(self.combo_box) + self.layout.addWidget(self.button_box) + self.setLayout(self.layout) + + # signals and slots + self.button_box.accepted.connect(self.accept) + self.button_box.rejected.connect(self.close) + self.combo_box.editTextChanged.connect(self.check_text) + + def check_text(self, text): + """Disable empty layout name possibility""" + if to_text_string(text) == u'': + self.button_ok.setEnabled(False) + else: + self.button_ok.setEnabled(True) + + +class LayoutSettingsDialog(QDialog): + """Layout settings dialog""" + def __init__(self, parent, names, order, active): + super(LayoutSettingsDialog, self).__init__(parent) + + # variables + self._parent = parent + self._selection_model = None + self.names = names + self.order = order + self.active = active + + # widgets + self.button_move_up = QPushButton(_('Move Up')) + self.button_move_down = QPushButton(_('Move Down')) + self.button_delete = QPushButton(_('Delete Layout')) + self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | + QDialogButtonBox.Cancel, + Qt.Horizontal, self) + self.group_box = QGroupBox(_("Layout Display and Order")) + self.table = QTableView(self) + self.ok_button = self.button_box.button(QDialogButtonBox.Ok) + self.cancel_button = self.button_box.button(QDialogButtonBox.Cancel) + self.cancel_button.setDefault(True) + self.cancel_button.setAutoDefault(True) + + # widget setup + self.dialog_size = QSize(300, 200) + self.setMinimumSize(self.dialog_size) + self.setFixedSize(self.dialog_size) + self.setWindowTitle('Layout Settings') + + self.table.setModel(LayoutModel(self.table, order, active)) + self.table.setSelectionBehavior(QAbstractItemView.SelectRows) + self.table.setSelectionMode(QAbstractItemView.SingleSelection) + self.table.verticalHeader().hide() + self.table.horizontalHeader().hide() + self.table.setAlternatingRowColors(True) + self.table.setShowGrid(False) + self.table.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + self.table.horizontalHeader().setStretchLastSection(True) + self.table.setColumnHidden(1, True) + + # need to keep a reference for pyside not to segfault! + self._selection_model = self.table.selectionModel() + + # layout + buttons_layout = QVBoxLayout() + buttons_layout.addWidget(self.button_move_up) + buttons_layout.addWidget(self.button_move_down) + buttons_layout.addStretch() + buttons_layout.addWidget(self.button_delete) + + group_layout = QHBoxLayout() + group_layout.addWidget(self.table) + group_layout.addLayout(buttons_layout) + self.group_box.setLayout(group_layout) + + layout = QVBoxLayout() + layout.addWidget(self.group_box) + layout.addWidget(self.button_box) + + self.setLayout(layout) + + # signals and slots + self.button_box.accepted.connect(self.accept) + self.button_box.rejected.connect(self.close) + self.button_delete.clicked.connect(self.delete_layout) + self.button_move_up.clicked.connect(lambda: self.move_layout(True)) + self.button_move_down.clicked.connect(lambda: self.move_layout(False)) + self.table.model().dataChanged.connect( + lambda: self.selection_changed(None, None)) + self._selection_model.selectionChanged.connect( + lambda: self.selection_changed(None, None)) + + # focus table + index = self.table.model().index(0, 0) + self.table.setCurrentIndex(index) + self.table.setFocus() + + def delete_layout(self): + """ """ + names, order, active = self.names, self.order, self.order + name = from_qvariant(self.table.selectionModel().currentIndex().data(), + to_text_string) + + if name in names: + index = names.index(name) + # In case nothing has focus in the table + if index != -1: + order.remove(name) + names[index] = None + if name in active: + active.remove(name) + self.names, self.order, self.active = names, order, active + self.table.model().set_data(order, active) + index = self.table.model().index(0, 0) + self.table.setCurrentIndex(index) + self.table.setFocus() + self.selection_changed(None, None) + if len(order) == 0: + self.button_move_up.setDisabled(True) + self.button_move_down.setDisabled(True) + self.button_delete.setDisabled(True) + + def move_layout(self, up=True): + """ """ + names, order, active = self.names, self.order, self.active + row = self.table.selectionModel().currentIndex().row() + row_new = row + + if up: + row_new -= 1 + else: + row_new += 1 + + order[row], order[row_new] = order[row_new], order[row] + + self.order = order + self.table.model().set_data(order, active) + index = self.table.model().index(row_new, 0) + self.table.setCurrentIndex(index) + self.table.setFocus() + self.selection_changed(None, None) + + def selection_changed(self, selection, deselection): + """ """ + model = self.table.model() + index = self.table.currentIndex() + row = index.row() + order, names, active = self.order, self.names, self.active + + state = model.row(row)[1] + name = model.row(row)[0] + + # Check if name changed + if name not in names: # Did changed + if row != -1: # row == -1, means no items left to delete + old_name = order[row] + order[row] = name + names[names.index(old_name)] = name + if old_name in active: + active[active.index(old_name)] = name + + # Check if checbox clicked + if state: + if name not in active: + active.append(name) + else: + if name in active: + active.remove(name) + + self.active = active + self.button_move_up.setDisabled(False) + self.button_move_down.setDisabled(False) + + if row == 0: + self.button_move_up.setDisabled(True) + if row == len(names) - 1: + self.button_move_down.setDisabled(True) + if len(names) == 0: + self.button_move_up.setDisabled(True) + self.button_move_down.setDisabled(True) + + +def test(): + """Run layout test widget test""" + from spyder.utils.qthelpers import qapplication + + app = qapplication() + names = ['test', 'tester', '20', '30', '40'] + order = ['test', 'tester', '20', '30', '40'] + active = ['test', 'tester'] + widget_1 = LayoutSettingsDialog(None, names, order, active) + widget_2 = LayoutSaveDialog(None, order) + widget_1.show() + widget_2.show() + sys.exit(app.exec_()) + +if __name__ == '__main__': + test() diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/plugins/maininterpreter.py spyder-3.0.2+dfsg1/spyder/plugins/maininterpreter.py --- spyder-2.3.8+dfsg1/spyder/plugins/maininterpreter.py 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/plugins/maininterpreter.py 2016-11-17 04:39:40.000000000 +0100 @@ -0,0 +1,218 @@ +# -*- coding: utf-8 -*- +# +# Copyright © Spyder Project Contributors +# Licensed under the terms of the MIT License +# (see spyder/__init__.py for details) + +"""Shortcut management""" + +# Standard library imports +from __future__ import print_function + +import os +import os.path as osp +import sys + +# Third party imports +from qtpy.QtWidgets import (QButtonGroup, QGroupBox, QInputDialog, QLabel, + QLineEdit, QMessageBox, QPushButton, QVBoxLayout) + +# Local imports +from spyder.config.base import _ +from spyder.plugins.configdialog import GeneralConfigPage +from spyder.py3compat import PY2, is_text_string, to_text_string +from spyder.utils import icon_manager as ima +from spyder.utils.misc import get_python_executable +from spyder.utils import programs + + +class MainInterpreterConfigPage(GeneralConfigPage): + CONF_SECTION = "main_interpreter" + NAME = _("Python interpreter") + ICON = ima.icon('python') + + def __init__(self, parent, main): + GeneralConfigPage.__init__(self, parent, main) + self.cus_exec_radio = None + self.pyexec_edit = None + + # Python executable selection (initializing default values as well) + executable = self.get_option('executable', get_python_executable()) + if self.get_option('default'): + executable = get_python_executable() + + if not osp.isfile(executable): + # This is absolutely necessary, in case the Python interpreter + # executable has been moved since last Spyder execution (following + # a Python distribution upgrade for example) + self.set_option('executable', get_python_executable()) + elif executable.endswith('pythonw.exe'): + # That should not be necessary because this case is already taken + # care of by the `get_python_executable` function but, this was + # implemented too late, so we have to fix it here too, in case + # the Python executable has already been set with pythonw.exe: + self.set_option('executable', + executable.replace("pythonw.exe", "python.exe")) + + def initialize(self): + GeneralConfigPage.initialize(self) + self.pyexec_edit.textChanged.connect(self.python_executable_changed) + self.cus_exec_radio.toggled.connect(self.python_executable_switched) + + def setup_page(self): + newcb = self.create_checkbox + + # Python executable Group + pyexec_group = QGroupBox(_("Python interpreter")) + pyexec_bg = QButtonGroup(pyexec_group) + pyexec_label = QLabel(_("Select the Python interpreter for all Spyder " + "consoles")) + def_exec_radio = self.create_radiobutton( + _("Default (i.e. the same as Spyder's)"), + 'default', button_group=pyexec_bg) + self.cus_exec_radio = self.create_radiobutton( + _("Use the following Python interpreter:"), + 'custom', button_group=pyexec_bg) + if os.name == 'nt': + filters = _("Executables")+" (*.exe)" + else: + filters = None + pyexec_file = self.create_browsefile('', 'executable', filters=filters) + for le in self.lineedits: + if self.lineedits[le][0] == 'executable': + self.pyexec_edit = le + def_exec_radio.toggled.connect(pyexec_file.setDisabled) + self.cus_exec_radio.toggled.connect(pyexec_file.setEnabled) + pyexec_layout = QVBoxLayout() + pyexec_layout.addWidget(pyexec_label) + pyexec_layout.addWidget(def_exec_radio) + pyexec_layout.addWidget(self.cus_exec_radio) + pyexec_layout.addWidget(pyexec_file) + pyexec_group.setLayout(pyexec_layout) + + # UMR Group + umr_group = QGroupBox(_("User Module Reloader (UMR)")) + umr_label = QLabel(_("UMR forces Python to reload modules which were " + "imported when executing a file in a Python or " + "IPython console with the runfile " + "function.")) + umr_label.setWordWrap(True) + umr_enabled_box = newcb(_("Enable UMR"), 'umr/enabled', + msg_if_enabled=True, msg_warning=_( + "This option will enable the User Module Reloader (UMR) " + "in Python/IPython consoles. UMR forces Python to " + "reload deeply modules during import when running a " + "Python script using the Spyder's builtin function " + "runfile." + "

    1. UMR may require to restart the " + "console in which it will be called " + "(otherwise only newly imported modules will be " + "reloaded when executing files)." + "

    2. If errors occur when re-running a " + "PyQt-based program, please check that the Qt objects " + "are properly destroyed (e.g. you may have to use the " + "attribute Qt.WA_DeleteOnClose on your main " + "window, using the setAttribute method)"), + ) + umr_verbose_box = newcb(_("Show reloaded modules list"), + 'umr/verbose', msg_info=_( + "Please note that these changes will " + "be applied only to new consoles")) + umr_namelist_btn = QPushButton( + _("Set UMR excluded (not reloaded) modules")) + umr_namelist_btn.clicked.connect(self.set_umr_namelist) + + umr_layout = QVBoxLayout() + umr_layout.addWidget(umr_label) + umr_layout.addWidget(umr_enabled_box) + umr_layout.addWidget(umr_verbose_box) + umr_layout.addWidget(umr_namelist_btn) + umr_group.setLayout(umr_layout) + + vlayout = QVBoxLayout() + vlayout.addWidget(pyexec_group) + vlayout.addWidget(umr_group) + vlayout.addStretch(1) + self.setLayout(vlayout) + + def python_executable_changed(self, pyexec): + """Custom Python executable value has been changed""" + if not self.cus_exec_radio.isChecked(): + return + if not is_text_string(pyexec): + pyexec = to_text_string(pyexec.toUtf8(), 'utf-8') + self.warn_python_compatibility(pyexec) + + def python_executable_switched(self, custom): + """Python executable default/custom radio button has been toggled""" + def_pyexec = get_python_executable() + cust_pyexec = self.pyexec_edit.text() + if not is_text_string(cust_pyexec): + cust_pyexec = to_text_string(cust_pyexec.toUtf8(), 'utf-8') + if def_pyexec != cust_pyexec: + if custom: + self.warn_python_compatibility(cust_pyexec) + + def warn_python_compatibility(self, pyexec): + if not osp.isfile(pyexec): + return + spyder_version = sys.version_info[0] + try: + args = ["-c", "import sys; print(sys.version_info[0])"] + proc = programs.run_program(pyexec, args) + console_version = int(proc.communicate()[0]) + except IOError: + console_version = spyder_version + if spyder_version != console_version: + QMessageBox.warning(self, _('Warning'), + _("You selected a Python %d interpreter for the console " + "but Spyder is running on Python %d!.

    " + "Although this is possible, we recommend you to install and " + "run Spyder directly with your selected interpreter, to avoid " + "seeing false warnings and errors due to the incompatible " + "syntax between these two Python versions." + ) % (console_version, spyder_version), QMessageBox.Ok) + + def set_umr_namelist(self): + """Set UMR excluded modules name list""" + arguments, valid = QInputDialog.getText(self, _('UMR'), + _("Set the list of excluded modules as " + "this: numpy, scipy"), + QLineEdit.Normal, + ", ".join(self.get_option('umr/namelist'))) + if valid: + arguments = to_text_string(arguments) + if arguments: + namelist = arguments.replace(' ', '').split(',') + fixed_namelist = [] + non_ascii_namelist = [] + for module_name in namelist: + if PY2: + if all(ord(c) < 128 for c in module_name): + if programs.is_module_installed(module_name): + fixed_namelist.append(module_name) + else: + QMessageBox.warning(self, _('Warning'), + _("You are working with Python 2, this means that " + "you can not import a module that contains non-" + "ascii characters."), QMessageBox.Ok) + non_ascii_namelist.append(module_name) + elif programs.is_module_installed(module_name): + fixed_namelist.append(module_name) + invalid = ", ".join(set(namelist)-set(fixed_namelist)- + set(non_ascii_namelist)) + if invalid: + QMessageBox.warning(self, _('UMR'), + _("The following modules are not " + "installed on your machine:\n%s" + ) % invalid, QMessageBox.Ok) + QMessageBox.information(self, _('UMR'), + _("Please note that these changes will " + "be applied only to new Python/IPython " + "consoles"), QMessageBox.Ok) + else: + fixed_namelist = [] + self.set_option('umr/namelist', fixed_namelist) + + def apply_settings(self, options): + self.main.apply_settings() diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/plugins/onlinehelp.py spyder-3.0.2+dfsg1/spyder/plugins/onlinehelp.py --- spyder-2.3.8+dfsg1/spyder/plugins/onlinehelp.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/plugins/onlinehelp.py 2016-11-19 01:31:58.000000000 +0100 @@ -1,20 +1,22 @@ # -*- coding: utf-8 -*- # -# Copyright © 2009-2010 Pierre Raybaut +# Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) +# (see spyder/__init__.py for details) """Online Help Plugin""" -from spyderlib.qt.QtCore import Signal - +# Standard library imports import os.path as osp +# Third party imports +from qtpy.QtCore import Signal + # Local imports -from spyderlib.baseconfig import get_conf_path, _ -from spyderlib.widgets.pydocgui import PydocBrowser -from spyderlib.plugins import SpyderPluginMixin -from spyderlib.py3compat import to_text_string +from spyder.config.base import _, get_conf_path +from spyder.plugins import SpyderPluginMixin +from spyder.py3compat import to_text_string +from spyder.widgets.pydocgui import PydocBrowser class OnlineHelp(PydocBrowser, SpyderPluginMixin): @@ -24,6 +26,7 @@ sig_option_changed = Signal(str, object) CONF_SECTION = 'onlinehelp' LOG_PATH = get_conf_path(CONF_SECTION) + def __init__(self, parent): self.main = parent PydocBrowser.__init__(self, parent) @@ -32,8 +35,8 @@ # Initialize plugin self.initialize_plugin() - self.register_widget_shortcuts("Editor", self.find_widget) - + self.register_widget_shortcuts(self.find_widget) + self.webview.set_zoom_factor(self.get_option('zoom_factor')) self.url_combo.setMaxCount(self.get_option('max_history_entries')) self.url_combo.addItems( self.load_history() ) diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/plugins/outlineexplorer.py spyder-3.0.2+dfsg1/spyder/plugins/outlineexplorer.py --- spyder-2.3.8+dfsg1/spyder/plugins/outlineexplorer.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/plugins/outlineexplorer.py 2016-11-19 01:31:58.000000000 +0100 @@ -1,29 +1,31 @@ # -*- coding: utf-8 -*- # -# Copyright © 2011 Pierre Raybaut +# Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) +# (see spyder/__init__.py for details) """Outline Explorer Plugin Data for outline are provided by method .get_outlineexplorer_data() of highlighter of assigned editor. For example, for Python files code editor uses -highlighter spyderlib.widgets.sourcecode.syntaxhighlighters.PythonSH +highlighter spyder.utils.syntaxhighlighters.PythonSH """ -from spyderlib.qt.QtCore import SIGNAL, Signal +# Third party imports +from qtpy.QtCore import Signal # Local imports -from spyderlib.baseconfig import _ -from spyderlib.utils.qthelpers import get_icon -from spyderlib.widgets.editortools import OutlineExplorerWidget -from spyderlib.plugins import SpyderPluginMixin -from spyderlib.py3compat import is_text_string +from spyder.config.base import _ +from spyder.plugins import SpyderPluginMixin +from spyder.py3compat import is_text_string +from spyder.utils import icon_manager as ima +from spyder.widgets.editortools import OutlineExplorerWidget class OutlineExplorer(OutlineExplorerWidget, SpyderPluginMixin): CONF_SECTION = 'outline_explorer' sig_option_changed = Signal(str, object) + def __init__(self, parent=None, fullpath_sorting=True): show_fullpath = self.get_option('show_fullpath') show_all_files = self.get_option('show_all_files') @@ -48,7 +50,7 @@ def get_plugin_icon(self): """Return widget icon""" - return get_icon('outline_explorer.png') + return ima.icon('outline_explorer') def get_focus_widget(self): """ @@ -63,8 +65,8 @@ def register_plugin(self): """Register plugin in Spyder's main window""" - self.connect(self.main, SIGNAL('restore_scrollbar_position()'), - self.restore_scrollbar_position) + self.main.restore_scrollbar_position.connect( + self.restore_scrollbar_position) self.main.add_dockwidget(self) def refresh_plugin(self): @@ -81,7 +83,7 @@ """DockWidget visibility has changed""" SpyderPluginMixin.visibility_changed(self, enable) if enable: - self.emit(SIGNAL("outlineexplorer_is_visible()")) + self.outlineexplorer_is_visible.emit() #------ Public API --------------------------------------------------------- def restore_scrollbar_position(self): diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/plugins/projectexplorer.py spyder-3.0.2+dfsg1/spyder/plugins/projectexplorer.py --- spyder-2.3.8+dfsg1/spyder/plugins/projectexplorer.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/plugins/projectexplorer.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,150 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright © 2010 Pierre Raybaut -# Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) - -"""Project Explorer Plugin""" - -from spyderlib.qt.QtGui import QFontDialog -from spyderlib.qt.QtCore import SIGNAL - -# Local imports -from spyderlib.baseconfig import _ -from spyderlib.utils.qthelpers import get_icon, create_action -from spyderlib.widgets.projectexplorer import ProjectExplorerWidget -from spyderlib.plugins import SpyderPluginMixin -from spyderlib.py3compat import is_text_string - - -class ProjectExplorer(ProjectExplorerWidget, SpyderPluginMixin): - """Project explorer plugin""" - CONF_SECTION = 'project_explorer' - - def __init__(self, parent=None): - ProjectExplorerWidget.__init__(self, parent=parent, - name_filters=self.get_option('name_filters'), - show_all=self.get_option('show_all', False), - show_hscrollbar=self.get_option('show_hscrollbar')) - SpyderPluginMixin.__init__(self, parent) - - # Initialize plugin - self.initialize_plugin() - - self.treewidget.header().hide() - self.set_font(self.get_plugin_font()) - self.load_config() - - #------ SpyderPluginWidget API --------------------------------------------- - def get_plugin_title(self): - """Return widget title""" - return _("Project explorer") - - def get_focus_widget(self): - """ - Return the widget to give focus to when - this plugin's dockwidget is raised on top-level - """ - return self.treewidget - - def get_plugin_actions(self): - """Return a list of actions related to plugin""" - new_project_act = create_action(self, text=_('New project...'), - icon=get_icon('project_expanded.png'), - triggered=self.create_new_project) - - font_action = create_action(self, _("&Font..."), - None, 'font.png', _("Set font style"), - triggered=self.change_font) - self.treewidget.common_actions += (None, font_action) - - self.main.file_menu_actions.insert(1, new_project_act) - - return [] - - def register_plugin(self): - """Register plugin in Spyder's main window""" - self.main.pythonpath_changed() - self.connect(self.main, SIGNAL('restore_scrollbar_position()'), - self.restore_scrollbar_position) - self.connect(self, SIGNAL("pythonpath_changed()"), - self.main.pythonpath_changed) - self.connect(self, SIGNAL("projects_were_closed()"), - self.projects_were_closed) - self.connect(self, SIGNAL("create_module(QString)"), - self.main.editor.new) - self.connect(self, SIGNAL("edit(QString)"), self.main.editor.load) - self.connect(self, SIGNAL("removed(QString)"), - self.main.editor.removed) - self.connect(self, SIGNAL("removed_tree(QString)"), - self.main.editor.removed_tree) - self.connect(self, SIGNAL("renamed(QString,QString)"), - self.main.editor.renamed) - self.main.editor.set_projectexplorer(self) - self.main.add_dockwidget(self) - - self.sig_open_file.connect(self.main.open_file) - - def refresh_plugin(self): - """Refresh project explorer widget""" - pass - - def closing_plugin(self, cancelable=False): - """Perform actions before parent main window is closed""" - self.save_config() - self.closing_widget() - return True - - #------ Public API --------------------------------------------------------- - def create_new_project(self): - """Create new project""" - if self.dockwidget.isHidden(): - self.dockwidget.show() - self.dockwidget.raise_() - if not self.treewidget.new_project(): - # Notify dockwidget to schedule a repaint - self.dockwidget.update() - - def projects_were_closed(self): - """Project were just closed: checking if related files are opened in - the editor and closing them""" - for fname in self.main.editor.get_filenames(): - if self.treewidget.workspace.is_file_in_closed_project(fname): - self.main.editor.close_file_from_name(fname) - - def change_font(self): - """Change font""" - font, valid = QFontDialog.getFont(self.get_plugin_font(), self, - _("Select a new font")) - if valid: - self.set_font(font) - self.set_plugin_font(font) - - def set_font(self, font): - """Set project explorer widget font""" - self.treewidget.setFont(font) - - def save_config(self): - """Save configuration: opened projects & tree widget state""" - self.set_option('workspace', self.get_workspace()) - self.set_option('expanded_state', self.treewidget.get_expanded_state()) - self.set_option('scrollbar_position', - self.treewidget.get_scrollbar_position()) - - def load_config(self): - """Load configuration: opened projects & tree widget state""" - self.set_workspace(self.get_option('workspace', None)) - expanded_state = self.get_option('expanded_state', None) - # Sometimes the expanded state option may be truncated in .ini file - # (for an unknown reason), in this case it would be converted to a - # string by 'userconfig': - if is_text_string(expanded_state): - expanded_state = None - if expanded_state is not None: - self.treewidget.set_expanded_state(expanded_state) - - def restore_scrollbar_position(self): - """Restoring scrollbar position after main window is visible""" - scrollbar_pos = self.get_option('scrollbar_position', None) - if scrollbar_pos is not None: - self.treewidget.set_scrollbar_position(scrollbar_pos) diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/plugins/projects.py spyder-3.0.2+dfsg1/spyder/plugins/projects.py --- spyder-2.3.8+dfsg1/spyder/plugins/projects.py 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/plugins/projects.py 2016-11-19 01:31:58.000000000 +0100 @@ -0,0 +1,403 @@ +# -*- coding: utf-8 -*- +# +# Copyright © Spyder Project Contributors +# Licensed under the terms of the MIT License +# (see spyder/__init__.py for details) + +""" +Projects Plugin + +It handles closing, opening and switching among projetcs and also +updating the file tree explorer associated with a project +""" + +# Standard library imports +import os.path as osp + +# Third party imports +from qtpy.compat import getexistingdirectory +from qtpy.QtCore import Signal, Slot +from qtpy.QtWidgets import QMenu, QMessageBox + +# Local imports +from spyder.config.base import _, get_home_dir +from spyder.plugins import SpyderPluginMixin +from spyder.py3compat import is_text_string, getcwd +from spyder.utils import icon_manager as ima +from spyder.utils.qthelpers import add_actions, create_action +from spyder.widgets.projects.explorer import ProjectExplorerWidget +from spyder.widgets.projects.projectdialog import ProjectDialog +from spyder.widgets.projects import EmptyProject + + +class Projects(ProjectExplorerWidget, SpyderPluginMixin): + """Projects plugin""" + + CONF_SECTION = 'project_explorer' + + open_terminal = Signal(str) + open_interpreter = Signal(str) + pythonpath_changed = Signal() + + # File operations + create_module = Signal(str) + edit = Signal(str) + removed = Signal(str) + removed_tree = Signal(str) + renamed = Signal(str, str) + redirect_stdio = Signal(bool) + + # Project handling + sig_project_created = Signal(object, object, object) + sig_project_loaded = Signal(object) + sig_project_closed = Signal(object) + + def __init__(self, parent=None): + ProjectExplorerWidget.__init__(self, parent=parent, + name_filters=self.get_option('name_filters'), + show_all=self.get_option('show_all'), + show_hscrollbar=self.get_option('show_hscrollbar')) + SpyderPluginMixin.__init__(self, parent) + + self.recent_projects = self.get_option('recent_projects', default=[]) + self.current_active_project = None + self.latest_project = None + + self.editor = None + self.workingdirectory = None + + # Initialize plugin + self.initialize_plugin() + self.setup_project(self.get_active_project_path()) + + #------ SpyderPluginWidget API --------------------------------------------- + def get_plugin_title(self): + """Return widget title""" + return _("Project explorer") + + def get_focus_widget(self): + """ + Return the widget to give focus to when + this plugin's dockwidget is raised on top-level + """ + return self.treewidget + + def get_plugin_actions(self): + """Return a list of actions related to plugin""" + self.new_project_action = create_action(self, + _("New Project..."), + triggered=self.create_new_project) + self.open_project_action = create_action(self, + _("Open Project..."), + triggered=lambda v: self.open_project()) + self.close_project_action = create_action(self, + _("Close Project"), + triggered=self.close_project) + self.delete_project_action = create_action(self, + _("Delete Project"), + triggered=self.delete_project) + self.clear_recent_projects_action =\ + create_action(self, _("Clear this list"), + triggered=self.clear_recent_projects) + self.edit_project_preferences_action =\ + create_action(self, _("Project Preferences"), + triggered=self.edit_project_preferences) + self.recent_project_menu = QMenu(_("Recent Projects"), self) + explorer_action = self.toggle_view_action + + self.main.projects_menu_actions += [self.new_project_action, + None, + self.open_project_action, + self.close_project_action, + self.delete_project_action, + None, + self.recent_project_menu, + explorer_action] + + self.setup_menu_actions() + return [] + + def register_plugin(self): + """Register plugin in Spyder's main window""" + self.editor = self.main.editor + self.workingdirectory = self.main.workingdirectory + + self.main.pythonpath_changed() + self.main.restore_scrollbar_position.connect( + self.restore_scrollbar_position) + self.pythonpath_changed.connect(self.main.pythonpath_changed) + self.create_module.connect(self.editor.new) + self.edit.connect(self.editor.load) + self.removed.connect(self.editor.removed) + self.removed_tree.connect(self.editor.removed_tree) + self.renamed.connect(self.editor.renamed) + self.editor.set_projects(self) + self.main.add_dockwidget(self) + + self.sig_open_file.connect(self.main.open_file) + + # New project connections. Order matters! + self.sig_project_loaded.connect( + lambda v: self.workingdirectory.chdir(v)) + self.sig_project_loaded.connect( + lambda v: self.main.update_window_title()) + self.sig_project_loaded.connect( + lambda v: self.editor.setup_open_files()) + self.sig_project_loaded.connect(self.update_explorer) + self.sig_project_closed[object].connect( + lambda v: self.workingdirectory.chdir(self.get_last_working_dir())) + self.sig_project_closed.connect( + lambda v: self.main.update_window_title()) + self.sig_project_closed.connect( + lambda v: self.editor.setup_open_files()) + self.recent_project_menu.aboutToShow.connect(self.setup_menu_actions) + + def refresh_plugin(self): + """Refresh project explorer widget""" + pass + + def closing_plugin(self, cancelable=False): + """Perform actions before parent main window is closed""" + self.save_config() + self.closing_widget() + return True + + #------ Public API --------------------------------------------------------- + def setup_menu_actions(self): + """Setup and update the menu actions.""" + self.recent_project_menu.clear() + self.recent_projects_actions = [] + if self.recent_projects: + for project in self.recent_projects: + if self.is_valid_project(project): + name = project.replace(get_home_dir(), '~') + action = create_action(self, + name, + icon = ima.icon('project'), + triggered=lambda v, path=project: self.open_project(path=path)) + self.recent_projects_actions.append(action) + else: + self.recent_projects.remove(project) + self.recent_projects_actions += [None, + self.clear_recent_projects_action] + else: + self.recent_projects_actions = [self.clear_recent_projects_action] + add_actions(self.recent_project_menu, self.recent_projects_actions) + self.update_project_actions() + + def update_project_actions(self): + """Update actions of the Projects menu""" + if self.recent_projects: + self.clear_recent_projects_action.setEnabled(True) + else: + self.clear_recent_projects_action.setEnabled(False) + + active = bool(self.get_active_project_path()) + self.close_project_action.setEnabled(active) + self.delete_project_action.setEnabled(active) + self.edit_project_preferences_action.setEnabled(active) + + def edit_project_preferences(self): + """Edit Spyder active project preferences""" + from spyder.widgets.projects.configdialog import ProjectPreferences + if self.project_active: + active_project = self.project_list[0] + dlg = ProjectPreferences(self, active_project) +# dlg.size_change.connect(self.set_project_prefs_size) +# if self.projects_prefs_dialog_size is not None: +# dlg.resize(self.projects_prefs_dialog_size) + dlg.show() +# dlg.check_all_settings() +# dlg.pages_widget.currentChanged.connect(self.__preference_page_changed) + dlg.exec_() + + @Slot() + def create_new_project(self): + """Create new project""" + active_project = self.current_active_project + dlg = ProjectDialog(self) + dlg.sig_project_creation_requested.connect(self._create_project) + dlg.sig_project_creation_requested.connect(self.sig_project_created) + if dlg.exec_(): + pass + if active_project is None: + self.show_explorer() + self.pythonpath_changed.emit() + self.restart_consoles() + + def _create_project(self, path): + """Create a new project.""" + self.open_project(path=path) + self.setup_menu_actions() + self.add_to_recent(path) + + def open_project(self, path=None, restart_consoles=True, + save_previous_files=True): + """Open the project located in `path`""" + if path is None: + basedir = get_home_dir() + path = getexistingdirectory(parent=self, + caption=_("Open project"), + basedir=basedir) + if not self.is_valid_project(path): + if path: + QMessageBox.critical(self, _('Error'), + _("%s is not a Spyder project!") % path) + return + else: + self.add_to_recent(path) + + # A project was not open before + if self.current_active_project is None: + if save_previous_files: + self.editor.save_open_files() + self.editor.set_option('last_working_dir', getcwd()) + self.show_explorer() + else: # we are switching projects + self.set_project_filenames(self.editor.get_open_filenames()) + + self.current_active_project = EmptyProject(path) + self.latest_project = EmptyProject(path) + self.set_option('current_project_path', self.get_active_project_path()) + self.setup_menu_actions() + self.sig_project_loaded.emit(path) + self.pythonpath_changed.emit() + if restart_consoles: + self.restart_consoles() + + def close_project(self): + """ + Close current project and return to a window without an active + project + """ + if self.current_active_project: + path = self.current_active_project.root_path + self.set_project_filenames(self.editor.get_open_filenames()) + self.current_active_project = None + self.set_option('current_project_path', None) + self.setup_menu_actions() + self.sig_project_closed.emit(path) + self.pythonpath_changed.emit() + self.dockwidget.close() + self.clear() + self.restart_consoles() + + def clear_recent_projects(self): + """Clear the list of recent projects""" + self.recent_projects = [] + self.setup_menu_actions() + + def get_active_project(self): + """Get the active project""" + return self.current_active_project + + def reopen_last_project(self): + """ + Reopen the active project when Spyder was closed last time, if any + """ + current_project_path = self.get_option('current_project_path', + default=None) + + # Needs a safer test of project existence! + if current_project_path and \ + self.is_valid_project(current_project_path): + self.open_project(path=current_project_path, + restart_consoles=False, + save_previous_files=False) + self.load_config() + + def get_project_filenames(self): + """Get the list of recent filenames of a project""" + recent_files = [] + if self.current_active_project: + recent_files = self.current_active_project.get_recent_files() + elif self.latest_project: + recent_files = self.latest_project.get_recent_files() + return recent_files + + def set_project_filenames(self, recent_files): + """Set the list of open file names in a project""" + if self.current_active_project: + self.current_active_project.set_recent_files(recent_files) + + def get_active_project_path(self): + """Get path of the active project""" + active_project_path = None + if self.current_active_project: + active_project_path = self.current_active_project.root_path + return active_project_path + + def get_pythonpath(self, at_start=False): + """Get project path as a list to be added to PYTHONPATH""" + if at_start: + current_path = self.get_option('current_project_path', + default=None) + else: + current_path = self.get_active_project_path() + if current_path is None: + return [] + else: + return [current_path] + + def get_last_working_dir(self): + """Get the path of the last working directory""" + return self.editor.get_option('last_working_dir', default=getcwd()) + + def save_config(self): + """Save configuration: opened projects & tree widget state""" + self.set_option('recent_projects', self.recent_projects) + self.set_option('expanded_state', self.treewidget.get_expanded_state()) + self.set_option('scrollbar_position', + self.treewidget.get_scrollbar_position()) + + def load_config(self): + """Load configuration: opened projects & tree widget state""" + expanded_state = self.get_option('expanded_state', None) + # Sometimes the expanded state option may be truncated in .ini file + # (for an unknown reason), in this case it would be converted to a + # string by 'userconfig': + if is_text_string(expanded_state): + expanded_state = None + if expanded_state is not None: + self.treewidget.set_expanded_state(expanded_state) + + def restore_scrollbar_position(self): + """Restoring scrollbar position after main window is visible""" + scrollbar_pos = self.get_option('scrollbar_position', None) + if scrollbar_pos is not None: + self.treewidget.set_scrollbar_position(scrollbar_pos) + + def update_explorer(self): + """Update explorer tree""" + self.setup_project(self.get_active_project_path()) + + def show_explorer(self): + """Show the explorer""" + if self.dockwidget.isHidden(): + self.dockwidget.show() + self.dockwidget.raise_() + self.dockwidget.update() + + def restart_consoles(self): + """Restart consoles when closing, opening and switching projects""" + self.main.extconsole.restart() + if self.main.ipyconsole: + self.main.ipyconsole.restart() + + def is_valid_project(self, path): + """Check if a directory is a valid Spyder project""" + spy_project_dir = osp.join(path, '.spyproject') + if osp.isdir(path) and osp.isdir(spy_project_dir): + return True + else: + return False + + def add_to_recent(self, project): + """ + Add an entry to recent projetcs + + We only maintain the list of the 10 most recent projects + """ + if project not in self.recent_projects: + self.recent_projects.insert(0, project) + self.recent_projects = self.recent_projects[:10] diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/plugins/runconfig.py spyder-3.0.2+dfsg1/spyder/plugins/runconfig.py --- spyder-2.3.8+dfsg1/spyder/plugins/runconfig.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/plugins/runconfig.py 2016-10-25 02:05:23.000000000 +0200 @@ -1,28 +1,29 @@ # -*- coding: utf-8 -*- # -# Copyright © 2010 Pierre Raybaut +# Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) +# (see spyder/__init__.py for details) """Run configurations related dialogs and widgets and data models""" -from spyderlib.qt.QtGui import (QVBoxLayout, QDialog, QWidget, QGroupBox, - QLabel, QPushButton, QCheckBox, QLineEdit, - QComboBox, QHBoxLayout, QDialogButtonBox, - QStackedWidget, QGridLayout, QSizePolicy, - QRadioButton, QMessageBox, QFrame, - QButtonGroup) -from spyderlib.qt.QtCore import SIGNAL, SLOT, Qt -from spyderlib.qt.compat import getexistingdirectory - +# Standard library imports import os.path as osp +# Third party imports +from qtpy.compat import getexistingdirectory +from qtpy.QtCore import QSize, Qt, Signal, Slot +from qtpy.QtWidgets import (QButtonGroup, QCheckBox, QComboBox, QDialog, + QDialogButtonBox, QFrame, QGridLayout, QGroupBox, + QHBoxLayout, QLabel, QLineEdit, QMessageBox, + QPushButton, QRadioButton, QSizePolicy, + QStackedWidget, QVBoxLayout, QWidget) + # Local imports -from spyderlib.baseconfig import _ -from spyderlib.config import CONF -from spyderlib.utils.qthelpers import get_icon, get_std_icon -from spyderlib.plugins.configdialog import GeneralConfigPage -from spyderlib.py3compat import to_text_string, getcwd +from spyder.config.base import _ +from spyder.config.main import CONF +from spyder.plugins.configdialog import GeneralConfigPage +from spyder.py3compat import getcwd, to_text_string +from spyder.utils import icon_manager as ima CURRENT_INTERPRETER = _("Execute in current Python or IPython console") @@ -51,7 +52,8 @@ self.current = None self.systerm = None self.interact = None - self.show_kill_warning = None + self.show_kill_warning =None + self.post_mortem = None self.python_args = None self.python_args_enabled = None self.set(CONF.get('run', 'defaultconfiguration', default={})) @@ -74,8 +76,12 @@ CONF.get('run', CURRENT_INTERPRETER_OPTION, True)) self.systerm = options.get('systerm', CONF.get('run', SYSTERM_INTERPRETER_OPTION, False)) - self.interact = options.get('interact', False) - self.show_kill_warning = options.get('show_kill_warning', True) + self.interact = options.get('interact', + CONF.get('run', 'interact', False)) + self.show_kill_warning = options.get('show_kill_warning', + CONF.get('run', 'show_kill_warning', False)) + self.post_mortem = options.get('post_mortem', + CONF.get('run', 'post_mortem', False)) self.python_args = options.get('python_args', '') self.python_args_enabled = options.get('python_args/enabled', False) @@ -89,6 +95,7 @@ 'systerm': self.systerm, 'interact': self.interact, 'show_kill_warning': self.show_kill_warning, + 'post_mortem': self.post_mortem, 'python_args/enabled': self.python_args_enabled, 'python_args': self.python_args, } @@ -156,23 +163,24 @@ self.clo_cb = QCheckBox(_("Command line options:")) common_layout.addWidget(self.clo_cb, 0, 0) self.clo_edit = QLineEdit() - self.connect(self.clo_cb, SIGNAL("toggled(bool)"), - self.clo_edit.setEnabled) + self.clo_cb.toggled.connect(self.clo_edit.setEnabled) self.clo_edit.setEnabled(False) common_layout.addWidget(self.clo_edit, 0, 1) self.wd_cb = QCheckBox(_("Working directory:")) common_layout.addWidget(self.wd_cb, 1, 0) wd_layout = QHBoxLayout() self.wd_edit = QLineEdit() - self.connect(self.wd_cb, SIGNAL("toggled(bool)"), - self.wd_edit.setEnabled) + self.wd_cb.toggled.connect(self.wd_edit.setEnabled) self.wd_edit.setEnabled(False) wd_layout.addWidget(self.wd_edit) - browse_btn = QPushButton(get_std_icon('DirOpenIcon'), "", self) + browse_btn = QPushButton(ima.icon('DirOpenIcon'), '', self) browse_btn.setToolTip(_("Select directory")) - self.connect(browse_btn, SIGNAL("clicked()"), self.select_directory) + browse_btn.clicked.connect(self.select_directory) wd_layout.addWidget(browse_btn) common_layout.addLayout(wd_layout, 1, 1) + self.post_mortem_cb = QCheckBox(_("Enter debugging mode when " + "errors appear during execution")) + common_layout.addWidget(self.post_mortem_cb) # --- Interpreter --- interpreter_group = QGroupBox(_("Console")) @@ -187,8 +195,7 @@ # --- Dedicated interpreter --- new_group = QGroupBox(_("Dedicated Python console")) - self.connect(self.current_radio, SIGNAL("toggled(bool)"), - new_group.setDisabled) + self.current_radio.toggled.connect(new_group.setDisabled) new_layout = QGridLayout() new_group.setLayout(new_layout) self.interact_cb = QCheckBox(_("Interact with the Python " @@ -197,18 +204,17 @@ self.show_kill_warning_cb = QCheckBox(_("Show warning when killing" " running process")) + new_layout.addWidget(self.show_kill_warning_cb, 2, 0, 1, -1) self.pclo_cb = QCheckBox(_("Command line options:")) new_layout.addWidget(self.pclo_cb, 3, 0) self.pclo_edit = QLineEdit() - self.connect(self.pclo_cb, SIGNAL("toggled(bool)"), - self.pclo_edit.setEnabled) + self.pclo_cb.toggled.connect(self.pclo_edit.setEnabled) self.pclo_edit.setEnabled(False) self.pclo_edit.setToolTip(_("-u is added to the " "other options you set here")) new_layout.addWidget(self.pclo_edit, 3, 1) - #TODO: Add option for "Post-mortem debugging" # Checkbox to preserve the old behavior, i.e. always open the dialog # on first run @@ -216,8 +222,7 @@ hline.setFrameShape(QFrame.HLine) hline.setFrameShadow(QFrame.Sunken) self.firstrun_cb = QCheckBox(ALWAYS_OPEN_FIRST_RUN % _("this dialog")) - self.connect(self.firstrun_cb, SIGNAL("clicked(bool)"), - self.set_firstrun_o) + self.firstrun_cb.clicked.connect(self.set_firstrun_o) self.firstrun_cb.setChecked(firstrun_o) layout = QVBoxLayout() @@ -252,6 +257,7 @@ self.dedicated_radio.setChecked(True) self.interact_cb.setChecked(self.runconf.interact) self.show_kill_warning_cb.setChecked(self.runconf.show_kill_warning) + self.post_mortem_cb.setChecked(self.runconf.post_mortem) self.pclo_cb.setChecked(self.runconf.python_args_enabled) self.pclo_edit.setText(self.runconf.python_args) @@ -264,6 +270,7 @@ self.runconf.systerm = self.systerm_radio.isChecked() self.runconf.interact = self.interact_cb.isChecked() self.runconf.show_kill_warning = self.show_kill_warning_cb.isChecked() + self.runconf.post_mortem = self.post_mortem_cb.isChecked() self.runconf.python_args_enabled = self.pclo_cb.isChecked() self.runconf.python_args = to_text_string(self.pclo_edit.text()) return self.runconf.get() @@ -285,6 +292,8 @@ class BaseRunConfigDialog(QDialog): """Run configuration dialog box, base widget""" + size_change = Signal(QSize) + def __init__(self, parent=None): QDialog.__init__(self, parent) @@ -294,7 +303,7 @@ # a segmentation fault on UNIX or an application crash on Windows self.setAttribute(Qt.WA_DeleteOnClose) - self.setWindowIcon(get_icon("run_settings.png")) + self.setWindowIcon(ima.icon('run_settings')) layout = QVBoxLayout() self.setLayout(layout) @@ -311,9 +320,9 @@ """Create dialog button box and add it to the dialog layout""" bbox = QDialogButtonBox(stdbtns) run_btn = bbox.addButton(_("Run"), QDialogButtonBox.AcceptRole) - self.connect(run_btn, SIGNAL('clicked()'), self.run_btn_clicked) - self.connect(bbox, SIGNAL("accepted()"), SLOT("accept()")) - self.connect(bbox, SIGNAL("rejected()"), SLOT("reject()")) + run_btn.clicked.connect(self.run_btn_clicked) + bbox.accepted.connect(self.accept) + bbox.rejected.connect(self.reject) btnlayout = QHBoxLayout() btnlayout.addStretch(1) btnlayout.addWidget(bbox) @@ -325,7 +334,7 @@ main application """ QDialog.resizeEvent(self, event) - self.emit(SIGNAL("size_change(QSize)"), self.size()) + self.size_change.emit(self.size()) def run_btn_clicked(self): """Run button was just clicked""" @@ -351,7 +360,8 @@ self.add_widgets(self.runconfigoptions) self.add_button_box(QDialogButtonBox.Cancel) self.setWindowTitle(_("Run settings for %s") % osp.basename(fname)) - + + @Slot() def accept(self): """Reimplement Qt method""" if not self.runconfigoptions.is_valid(): @@ -404,8 +414,7 @@ widget.set(options) self.combo.addItem(filename) self.stack.addWidget(widget) - self.connect(self.combo, SIGNAL("currentIndexChanged(int)"), - self.stack.setCurrentIndex) + self.combo.currentIndexChanged.connect(self.stack.setCurrentIndex) self.combo.setCurrentIndex(index) self.add_widgets(combo_label, self.combo, 10, self.stack) @@ -433,7 +442,7 @@ CONF_SECTION = "run" NAME = _("Run") - ICON = "run.png" + ICON = ima.icon('run') def setup_page(self): run_dlg = _("Run Settings") @@ -462,8 +471,8 @@ interpreter_layout.addWidget(self.dedicated_radio) interpreter_layout.addWidget(self.systerm_radio) - wdir_group = QGroupBox(_("Working directory")) - wdir_bg = QButtonGroup(wdir_group) + general_group = QGroupBox(_("General settings")) + wdir_bg = QButtonGroup(general_group) wdir_label = QLabel(_("Default working directory is:")) wdir_label.setWordWrap(True) dirname_radio = self.create_radiobutton(_("the script directory"), @@ -473,19 +482,35 @@ WDIR_USE_FIXED_DIR_OPTION, False, button_group=wdir_bg) thisdir_bd = self.create_browsedir("", WDIR_FIXED_DIR_OPTION, getcwd()) - self.connect(thisdir_radio, SIGNAL("toggled(bool)"), - thisdir_bd.setEnabled) - self.connect(dirname_radio, SIGNAL("toggled(bool)"), - thisdir_bd.setDisabled) + thisdir_radio.toggled.connect(thisdir_bd.setEnabled) + dirname_radio.toggled.connect(thisdir_bd.setDisabled) thisdir_layout = QHBoxLayout() thisdir_layout.addWidget(thisdir_radio) thisdir_layout.addWidget(thisdir_bd) - wdir_layout = QVBoxLayout() - wdir_layout.addWidget(wdir_label) - wdir_layout.addWidget(dirname_radio) - wdir_layout.addLayout(thisdir_layout) - wdir_group.setLayout(wdir_layout) + post_mortem = self.create_checkbox( + _("Enter debugging mode when errors appear during execution"), + 'post_mortem', False) + + general_layout = QVBoxLayout() + general_layout.addWidget(wdir_label) + general_layout.addWidget(dirname_radio) + general_layout.addLayout(thisdir_layout) + general_layout.addWidget(post_mortem) + general_group.setLayout(general_layout) + + dedicated_group = QGroupBox(_("Dedicated Python console")) + interact_after = self.create_checkbox( + _("Interact with the Python console after execution"), + 'interact', False) + show_warning = self.create_checkbox( + _("Show warning when killing running processes"), + 'show_kill_warning', True) + + dedicated_layout = QVBoxLayout() + dedicated_layout.addWidget(interact_after) + dedicated_layout.addWidget(show_warning) + dedicated_group.setLayout(dedicated_layout) firstrun_cb = self.create_checkbox( ALWAYS_OPEN_FIRST_RUN % _("Run Settings dialog"), @@ -495,7 +520,8 @@ vlayout.addWidget(about_label) vlayout.addSpacing(10) vlayout.addWidget(interpreter_group) - vlayout.addWidget(wdir_group) + vlayout.addWidget(general_group) + vlayout.addWidget(dedicated_group) vlayout.addWidget(firstrun_cb) vlayout.addStretch(1) self.setLayout(vlayout) diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/plugins/shortcuts.py spyder-3.0.2+dfsg1/spyder/plugins/shortcuts.py --- spyder-2.3.8+dfsg1/spyder/plugins/shortcuts.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/plugins/shortcuts.py 2016-10-25 02:05:23.000000000 +0200 @@ -1,176 +1,529 @@ # -*- coding: utf-8 -*- # -# Copyright © 2010 Pierre Raybaut +# Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) +# (see spyder/__init__.py for details) """Shortcut management""" +# Standard library imports from __future__ import print_function - -from spyderlib.qt.QtGui import (QVBoxLayout, QComboBox, QItemDelegate, - QTableView, QMessageBox, QPushButton) -from spyderlib.qt.QtCore import (Qt, QSize, QAbstractTableModel, QModelIndex, - SIGNAL) -from spyderlib.qt.compat import to_qvariant, from_qvariant - +import os +import re import sys -# Local imports -from spyderlib.baseconfig import _ -from spyderlib.guiconfig import (get_shortcut, set_shortcut, - iter_shortcuts, reset_shortcuts) -from spyderlib.plugins.configdialog import GeneralConfigPage -from spyderlib.py3compat import to_text_string, is_text_string +# Third party imports +from qtpy import PYQT5 +from qtpy.compat import from_qvariant, to_qvariant +from qtpy.QtCore import (QAbstractTableModel, QModelIndex, QRegExp, + QSortFilterProxyModel, Qt) +from qtpy.QtGui import (QKeySequence, QRegExpValidator) +from qtpy.QtWidgets import (QAbstractItemView, QApplication, QDialog, + QDialogButtonBox, QGridLayout, QHBoxLayout, QLabel, + QLineEdit, QMessageBox, QPushButton, QSpacerItem, + QTableView, QVBoxLayout) - -KEYSTRINGS = ["Escape", "Tab", "Backtab", "Backspace", "Return", "Enter", - "Delete", "Pause", "Print", "Clear", "Home", "End", "Left", +# Local imports +from spyder.config.base import _, debug_print +from spyder.config.gui import (get_shortcut, iter_shortcuts, + reset_shortcuts, set_shortcut) +from spyder.plugins.configdialog import GeneralConfigPage +from spyder.utils import icon_manager as ima +from spyder.utils.qthelpers import get_std_icon +from spyder.utils.stringmatching import get_search_scores, get_search_regex +from spyder.widgets.helperwidgets import HTMLDelegate +from spyder.widgets.helperwidgets import HelperToolButton + + +MODIFIERS = {Qt.Key_Shift: Qt.SHIFT, + Qt.Key_Control: Qt.CTRL, + Qt.Key_Alt: Qt.ALT, + Qt.Key_Meta: Qt.META} + +# Valid shortcut keys +SINGLE_KEYS = ["F{}".format(_i) for _i in range(1, 36)] + ["Delete", "Escape"] +KEYSTRINGS = ["Tab", "Backtab", "Backspace", "Return", "Enter", + "Pause", "Print", "Clear", "Home", "End", "Left", "Up", "Right", "Down", "PageUp", "PageDown"] + \ - ["F%d" % _i for _i in range(1, 36)] + \ - ["Space", "Exclam", "QuoteDbl", "NumberSign", "Dollar", "Percent", - "Ampersand", "Apostrophe", "ParenLeft", "ParenRight", "Asterisk", - "Plus", "Comma", "Minus", "Period", "Slash"] + \ + ["Space", "Exclam", "QuoteDbl", "NumberSign", "Dollar", + "Percent", "Ampersand", "Apostrophe", "ParenLeft", + "ParenRight", "Asterisk", "Plus", "Comma", "Minus", + "Period", "Slash"] + \ [str(_i) for _i in range(10)] + \ - ["Colon", "Semicolon", "Less", "Equal", "Greater", "Question", - "At"] + [chr(_i) for _i in range(65, 91)] + \ - ["BracketLeft", "Backslash", "BracketRight", "Underscore"] - - -class Key(object): - MODIFIERS = {Qt.NoModifier: "", Qt.ShiftModifier: "Shift", - Qt.ControlModifier: "Ctrl", Qt.AltModifier: "Alt", - Qt.MetaModifier: "Meta"} - if sys.platform == 'darwin': - MODIFIERNAMES = {Qt.NoModifier: "", Qt.ShiftModifier: "Shift", - Qt.ControlModifier: "Cmd", Qt.AltModifier: "Alt", - Qt.MetaModifier: "Ctrl"} - elif sys.platform == 'win32': - MODIFIERNAMES = {Qt.NoModifier: "", Qt.ShiftModifier: "Shift", - Qt.ControlModifier: "Ctrl", Qt.AltModifier: "Alt", - Qt.MetaModifier: "Win"} - else: - MODIFIERNAMES = {Qt.NoModifier: "", Qt.ShiftModifier: "Shift", - Qt.ControlModifier: "Ctrl", Qt.AltModifier: "Alt", - Qt.MetaModifier: "Meta"} - KEYS = {} - for attr in KEYSTRINGS: - KEYS[getattr(Qt, "Key_"+attr)] = attr - - def __init__(self, key, mod1=Qt.NoModifier, mod2=Qt.NoModifier, - mod3=Qt.NoModifier): - modifiers = [mod1, mod2, mod3] - assert all([mod in self.MODIFIERS for mod in modifiers]) - self.modifiers = sorted(modifiers) - assert key in self.KEYS - self.key = key - - def __str__(self): - tlist = [] - for mod in sorted(list(set(self.modifiers))): - if mod != Qt.NoModifier: - tlist.append(self.MODIFIERS[mod]) - tlist.append(self.KEYS[self.key]) - return "+".join(tlist) - - def __unicode__(self): - return to_text_string(self.__str__()) - - @staticmethod - def modifier_from_str(modstr): - for k, v in list(Key.MODIFIERS.items()): - if v.lower() == modstr.lower(): - return k - - @staticmethod - def key_from_str(keystr): - for k, v in list(Key.KEYS.items()): - if v.lower() == keystr.lower(): - return k - - @staticmethod - def modifier_from_name(modname): - for k, v in list(Key.MODIFIERNAMES.items()): - if v.lower() == modname.lower(): - return k - -def keystr2key(keystr): - keylist = keystr.split("+") - mods = [] - if len(keylist) > 1: - for modstr in keylist[:-1]: - mods.append(Key.modifier_from_str(modstr)) - return Key(Key.key_from_str(keylist[-1]), *mods) + ["Colon", "Semicolon", "Less", "Equal", "Greater", + "Question", "At"] + [chr(_i) for _i in range(65, 91)] + \ + ["BracketLeft", "Backslash", "BracketRight", "Underscore", + "Control", "Alt", "Shift", "Meta"] +VALID_SINGLE_KEYS = [getattr(Qt, 'Key_{0}'.format(k)) for k in SINGLE_KEYS] +VALID_KEYS = [getattr(Qt, 'Key_{0}'.format(k)) for k in KEYSTRINGS+SINGLE_KEYS] + +# Valid finder chars. To be improved +VALID_ACCENT_CHARS = "ÁÉÍOÚáéíúóàèìòùÀÈÌÒÙâêîôûÂÊÎÔÛäëïöüÄËÏÖÜñÑ" +VALID_FINDER_CHARS = "[A-Za-z\s{0}]".format(VALID_ACCENT_CHARS) + + +class CustomLineEdit(QLineEdit): + """QLineEdit that filters its key press and release events.""" + def __init__(self, parent): + super(CustomLineEdit, self).__init__(parent) + self.setReadOnly(True) + self.setFocusPolicy(Qt.NoFocus) + + def keyPressEvent(self, e): + """Qt Override""" + self.parent().keyPressEvent(e) + + def keyReleaseEvent(self, e): + """Qt Override""" + self.parent().keyReleaseEvent(e) + + +class ShortcutFinder(QLineEdit): + """Textbox for filtering listed shortcuts in the table.""" + def __init__(self, parent, callback=None): + super(ShortcutFinder, self).__init__(parent) + self._parent = parent + + # Widget setup + regex = QRegExp(VALID_FINDER_CHARS + "{100}") + self.setValidator(QRegExpValidator(regex)) + + # Signals + if callback: + self.textChanged.connect(callback) + + def set_text(self, text): + """Set the filter text.""" + text = text.strip() + new_text = self.text() + text + self.setText(new_text) + + def keyPressEvent(self, event): + """Qt Override.""" + key = event.key() + if key in [Qt.Key_Up]: + self._parent.previous_row() + elif key in [Qt.Key_Down]: + self._parent.next_row() + elif key in [Qt.Key_Enter, Qt.Key_Return]: + self._parent.show_editor() + else: + super(ShortcutFinder, self).keyPressEvent(event) + + +# Error codes for the shortcut editor dialog +NO_WARNING, SEQUENCE_LENGTH, SEQUENCE_CONFLICT, INVALID_KEY = [0, 1, 2, 3] + + +class ShortcutEditor(QDialog): + """A dialog for entering key sequences.""" + def __init__(self, parent, context, name, sequence, shortcuts): + super(ShortcutEditor, self).__init__(parent) + self._parent = parent + + self.context = context + self.npressed = 0 + self.keys = set() + self.key_modifiers = set() + self.key_non_modifiers = list() + self.key_text = list() + self.sequence = sequence + self.new_sequence = None + self.edit_state = True + self.shortcuts = shortcuts + + # Widgets + self.label_info = QLabel() + self.label_info.setText(_("Press the new shortcut and select 'Ok': \n" + "(Press 'Tab' once to switch focus between the shortcut entry \n" + "and the buttons below it)")) + self.label_current_sequence = QLabel(_("Current shortcut:")) + self.text_current_sequence = QLabel(sequence) + self.label_new_sequence = QLabel(_("New shortcut:")) + self.text_new_sequence = CustomLineEdit(self) + self.text_new_sequence.setPlaceholderText(sequence) + self.helper_button = HelperToolButton() + self.helper_button.hide() + self.label_warning = QLabel() + self.label_warning.hide() + + bbox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) + self.button_ok = bbox.button(QDialogButtonBox.Ok) + self.button_cancel = bbox.button(QDialogButtonBox.Cancel) + + # Setup widgets + self.setWindowTitle(_('Shortcut: {0}').format(name)) + self.button_ok.setFocusPolicy(Qt.NoFocus) + self.button_ok.setEnabled(False) + self.button_cancel.setFocusPolicy(Qt.NoFocus) + self.helper_button.setToolTip('') + self.helper_button.setFocusPolicy(Qt.NoFocus) + style = """ + QToolButton { + margin:1px; + border: 0px solid grey; + padding:0px; + border-radius: 0px; + }""" + self.helper_button.setStyleSheet(style) + self.text_new_sequence.setFocusPolicy(Qt.NoFocus) + self.label_warning.setFocusPolicy(Qt.NoFocus) + + # Layout + spacing = 5 + layout_sequence = QGridLayout() + layout_sequence.addWidget(self.label_info, 0, 0, 1, 3) + layout_sequence.addItem(QSpacerItem(spacing, spacing), 1, 0, 1, 2) + layout_sequence.addWidget(self.label_current_sequence, 2, 0) + layout_sequence.addWidget(self.text_current_sequence, 2, 2) + layout_sequence.addWidget(self.label_new_sequence, 3, 0) + layout_sequence.addWidget(self.helper_button, 3, 1) + layout_sequence.addWidget(self.text_new_sequence, 3, 2) + layout_sequence.addWidget(self.label_warning, 4, 2, 1, 2) + + layout = QVBoxLayout() + layout.addLayout(layout_sequence) + layout.addSpacing(spacing) + layout.addWidget(bbox) + self.setLayout(layout) + + # Signals + bbox.accepted.connect(self.accept) + bbox.rejected.connect(self.reject) + + def keyPressEvent(self, e): + """Qt override.""" + key = e.key() + # Check if valid keys + if key not in VALID_KEYS: + self.invalid_key_flag = True + return + + self.npressed += 1 + self.key_non_modifiers.append(key) + self.key_modifiers.add(key) + self.key_text.append(e.text()) + self.invalid_key_flag = False + + debug_print('key {0}, npressed: {1}'.format(key, self.npressed)) + + if key == Qt.Key_unknown: + return + + # The user clicked just and only the special keys + # Ctrl, Shift, Alt, Meta. + if (key == Qt.Key_Control or + key == Qt.Key_Shift or + key == Qt.Key_Alt or + key == Qt.Key_Meta): + return + + modifiers = e.modifiers() + if modifiers & Qt.ShiftModifier: + key += Qt.SHIFT + if modifiers & Qt.ControlModifier: + key += Qt.CTRL + if sys.platform == 'darwin': + self.npressed -= 1 + debug_print('decrementing') + if modifiers & Qt.AltModifier: + key += Qt.ALT + if modifiers & Qt.MetaModifier: + key += Qt.META + + self.keys.add(key) + + def toggle_state(self): + """Switch between shortcut entry and Accept/Cancel shortcut mode.""" + self.edit_state = not self.edit_state + + if not self.edit_state: + self.text_new_sequence.setEnabled(False) + if self.button_ok.isEnabled(): + self.button_ok.setFocus() + else: + self.button_cancel.setFocus() + else: + self.text_new_sequence.setEnabled(True) + self.text_new_sequence.setFocus() + + def nonedit_keyrelease(self, e): + """Key release event for non-edit state.""" + key = e.key() + if key in [Qt.Key_Escape]: + self.close() + return + + if key in [Qt.Key_Left, Qt.Key_Right, Qt.Key_Up, + Qt.Key_Down]: + if self.button_ok.hasFocus(): + self.button_cancel.setFocus() + else: + self.button_ok.setFocus() + + def keyReleaseEvent(self, e): + """Qt override.""" + self.npressed -= 1 + if self.npressed <= 0: + key = e.key() + + if len(self.keys) == 1 and key == Qt.Key_Tab: + self.toggle_state() + return + + if len(self.keys) == 1 and key == Qt.Key_Escape: + self.set_sequence('') + self.label_warning.setText(_("Please introduce a different " + "shortcut")) + + if len(self.keys) == 1 and key in [Qt.Key_Return, Qt.Key_Enter]: + self.toggle_state() + return + + if not self.edit_state: + self.nonedit_keyrelease(e) + else: + debug_print('keys: {}'.format(self.keys)) + if self.keys and key != Qt.Key_Escape: + self.validate_sequence() + self.keys = set() + self.key_modifiers = set() + self.key_non_modifiers = list() + self.key_text = list() + self.npressed = 0 + + def check_conflicts(self): + """Check shortcuts for conflicts.""" + conflicts = [] + for index, shortcut in enumerate(self.shortcuts): + sequence = str(shortcut.key) + if sequence == self.new_sequence and \ + (shortcut.context == self.context or shortcut.context == '_' or + self.context == '_'): + conflicts.append(shortcut) + return conflicts + + def update_warning(self, warning_type=NO_WARNING, conflicts=[]): + """Update warning label to reflect conflict status of new shortcut""" + if warning_type == NO_WARNING: + warn = False + tip = 'This shortcut is correct!' + elif warning_type == SEQUENCE_CONFLICT: + template = '{0}{1}' + tip_title = _('The new shorcut conflicts with:') + '
    ' + tip_body = '' + for s in conflicts: + tip_body += ' - {0}: {1}
    '.format(s.context, s.name) + tip_body = tip_body[:-4] # Removing last
    + tip = template.format(tip_title, tip_body) + warn = True + elif warning_type == SEQUENCE_LENGTH: + # Sequences with 5 keysequences (i.e. Ctrl+1, Ctrl+2, Ctrl+3, + # Ctrl+4, Ctrl+5) are invalid + template = '{0}' + tip = _('A compound sequence can have {break} a maximum of ' + '4 subsequences.{break}').format(**{'break': '
    '}) + warn = True + elif warning_type == INVALID_KEY: + template = '{0}' + tip = _('Invalid key entered') + '
    ' + warn = True + + self.helper_button.show() + if warn: + self.label_warning.show() + self.helper_button.setIcon(get_std_icon('MessageBoxWarning')) + self.button_ok.setEnabled(False) + else: + self.helper_button.setIcon(get_std_icon('DialogApplyButton')) + + self.label_warning.setText(tip) + + def set_sequence(self, sequence): + """Set the new shortcut and update buttons.""" + if not sequence or self.sequence == sequence: + self.button_ok.setEnabled(False) + different_sequence = False + else: + self.button_ok.setEnabled(True) + different_sequence = True + + if sys.platform == 'darwin': + if 'Meta+Ctrl' in sequence: + shown_sequence = sequence.replace('Meta+Ctrl', 'Ctrl+Cmd') + elif 'Ctrl+Meta' in sequence: + shown_sequence = sequence.replace('Ctrl+Meta', 'Cmd+Ctrl') + elif 'Ctrl' in sequence: + shown_sequence = sequence.replace('Ctrl', 'Cmd') + elif 'Meta' in sequence: + shown_sequence = sequence.replace('Meta', 'Ctrl') + else: + shown_sequence = sequence + else: + shown_sequence = sequence + self.text_new_sequence.setText(shown_sequence) + self.new_sequence = sequence + + conflicts = self.check_conflicts() + if conflicts and different_sequence: + warning_type = SEQUENCE_CONFLICT + else: + warning_type = NO_WARNING + + self.update_warning(warning_type=warning_type, conflicts=conflicts) + + def validate_sequence(self): + """Provide additional checks for accepting or rejecting shortcuts.""" + if self.invalid_key_flag: + self.update_warning(warning_type=INVALID_KEY) + return + + for mod in MODIFIERS: + non_mod = set(self.key_non_modifiers) + non_mod.discard(mod) + if mod in self.key_non_modifiers: + self.key_non_modifiers.remove(mod) + + self.key_modifiers = self.key_modifiers - non_mod + + while u'' in self.key_text: + self.key_text.remove(u'') + + self.key_text = [k.upper() for k in self.key_text] + + # Fix Backtab, Tab issue + if os.name == 'nt': + if Qt.Key_Backtab in self.key_non_modifiers: + idx = self.key_non_modifiers.index(Qt.Key_Backtab) + self.key_non_modifiers[idx] = Qt.Key_Tab + + if len(self.key_modifiers) == 0: + # Filter single key allowed + if self.key_non_modifiers[0] not in VALID_SINGLE_KEYS: + return + # Filter + elif len(self.key_non_modifiers) > 1: + return + + # QKeySequence accepts a maximum of 4 different sequences + if len(self.keys) > 4: + # Update warning + self.update_warning(warning_type=SEQUENCE_LENGTH) + return + + keys = [] + for i in range(len(self.keys)): + key_seq = 0 + for m in self.key_modifiers: + key_seq += MODIFIERS[m] + key_seq += self.key_non_modifiers[i] + keys.append(key_seq) + + sequence = QKeySequence(*keys) + + self.set_sequence(sequence.toString()) + class Shortcut(object): + """Shortcut convenience class for holding shortcut context, name, + original ordering index, key sequence for the shortcut and localized text. + """ def __init__(self, context, name, key=None): + self.index = 0 # Sorted index. Populated when loading shortcuts self.context = context self.name = name - if is_text_string(key): - key = keystr2key(key) self.key = key - + def __str__(self): - return "%s/%s: %s" % (self.context, self.name, self.key) - + return "{0}/{1}: {2}".format(self.context, self.name, self.key) + def load(self): - self.key = keystr2key(get_shortcut(self.context, self.name)) - + self.key = get_shortcut(self.context, self.name) + def save(self): - set_shortcut(self.context, self.name, str(self.key)) + set_shortcut(self.context, self.name, self.key) + +CONTEXT, NAME, SEQUENCE, SEARCH_SCORE = [0, 1, 2, 3] -CONTEXT, NAME, MOD1, MOD2, MOD3, KEY = list(range(6)) class ShortcutsModel(QAbstractTableModel): - def __init__(self): + def __init__(self, parent): QAbstractTableModel.__init__(self) + self._parent = parent + self.shortcuts = [] + self.scores = [] + self.rich_text = [] + self.normal_text = [] + self.letters = '' + self.label = QLabel() + self.widths = [] + + # Needed to compensate for the HTMLDelegate color selection unawarness + palette = parent.palette() + self.text_color = palette.text().color().name() + self.text_color_highlight = palette.highlightedText().color().name() + + def current_index(self): + """Get the currently selected index in the parent table view.""" + i = self._parent.proxy_model.mapToSource(self._parent.currentIndex()) + return i def sortByName(self): + """Qt Override.""" self.shortcuts = sorted(self.shortcuts, key=lambda x: x.context+'/'+x.name) self.reset() def flags(self, index): + """Qt Override.""" if not index.isValid(): return Qt.ItemIsEnabled - column = index.column() - if column in (MOD1, MOD2, MOD3, KEY): - return Qt.ItemFlags(QAbstractTableModel.flags(self, index)| - Qt.ItemIsEditable) - else: - return Qt.ItemFlags(QAbstractTableModel.flags(self, index)) + return Qt.ItemFlags(QAbstractTableModel.flags(self, index)) def data(self, index, role=Qt.DisplayRole): - if not index.isValid() or \ - not (0 <= index.row() < len(self.shortcuts)): + """Qt Override.""" + row = index.row() + if not index.isValid() or not (0 <= row < len(self.shortcuts)): return to_qvariant() - shortcut = self.shortcuts[index.row()] + + shortcut = self.shortcuts[row] key = shortcut.key column = index.column() + if role == Qt.DisplayRole: if column == CONTEXT: return to_qvariant(shortcut.context) elif column == NAME: - return to_qvariant(shortcut.name) - elif column == MOD1: - return to_qvariant(Key.MODIFIERNAMES[key.modifiers[0]]) - elif column == MOD2: - return to_qvariant(Key.MODIFIERNAMES[key.modifiers[1]]) - elif column == MOD3: - return to_qvariant(Key.MODIFIERNAMES[key.modifiers[2]]) - elif column == KEY: - return to_qvariant(Key.KEYS[key.key]) + color = self.text_color + if self._parent == QApplication.focusWidget(): + if self.current_index().row() == row: + color = self.text_color_highlight + else: + color = self.text_color + text = self.rich_text[row] + text = '

    {1}

    '.format(color, text) + return to_qvariant(text) + elif column == SEQUENCE: + text = QKeySequence(key).toString(QKeySequence.NativeText) + return to_qvariant(text) + elif column == SEARCH_SCORE: + # Treating search scores as a table column simplifies the + # sorting once a score for a specific string in the finder + # has been defined. This column however should always remain + # hidden. + return to_qvariant(self.scores[row]) elif role == Qt.TextAlignmentRole: - return to_qvariant(int(Qt.AlignHCenter|Qt.AlignVCenter)) + return to_qvariant(int(Qt.AlignHCenter | Qt.AlignVCenter)) return to_qvariant() def headerData(self, section, orientation, role=Qt.DisplayRole): + """Qt Override.""" if role == Qt.TextAlignmentRole: if orientation == Qt.Horizontal: - return to_qvariant(int(Qt.AlignHCenter|Qt.AlignVCenter)) - return to_qvariant(int(Qt.AlignRight|Qt.AlignVCenter)) + return to_qvariant(int(Qt.AlignHCenter | Qt.AlignVCenter)) + return to_qvariant(int(Qt.AlignRight | Qt.AlignVCenter)) if role != Qt.DisplayRole: return to_qvariant() if orientation == Qt.Horizontal: @@ -178,197 +531,312 @@ return to_qvariant(_("Context")) elif section == NAME: return to_qvariant(_("Name")) - elif section == MOD1: - return to_qvariant(_("Mod1")) - elif section == MOD2: - return to_qvariant(_("Mod2")) - elif section == MOD3: - return to_qvariant(_("Mod3")) - elif section == KEY: - return to_qvariant(_("Key")) + elif section == SEQUENCE: + return to_qvariant(_("Shortcut")) + elif section == SEARCH_SCORE: + return to_qvariant(_("Score")) return to_qvariant() def rowCount(self, index=QModelIndex()): + """Qt Override.""" return len(self.shortcuts) def columnCount(self, index=QModelIndex()): - return 6 - + """Qt Override.""" + return 4 + def setData(self, index, value, role=Qt.EditRole): + """Qt Override.""" if index.isValid() and 0 <= index.row() < len(self.shortcuts): shortcut = self.shortcuts[index.row()] - key = shortcut.key column = index.column() text = from_qvariant(value, str) - if column == MOD1: - key.modifiers[0] = Key.modifier_from_name(text) - elif column == MOD2: - key.modifiers[1] = Key.modifier_from_name(text) - elif column == MOD3: - key.modifiers[2] = Key.modifier_from_name(text) - elif column == KEY: - key.key = Key.key_from_str(text) - self.emit(SIGNAL("dataChanged(QModelIndex,QModelIndex)"), - index, index) + if column == SEQUENCE: + shortcut.key = text + self.dataChanged.emit(index, index) return True return False + def update_search_letters(self, text): + """Update search letters with text input in search box.""" + self.letters = text + names = [shortcut.name for shortcut in self.shortcuts] + results = get_search_scores(text, names, template='{0}') + self.normal_text, self.rich_text, self.scores = zip(*results) + self.reset() + + def update_active_row(self): + """Update active row to update color in selected text.""" + self.data(self.current_index()) + + def row(self, row_num): + """Get row based on model index. Needed for the custom proxy model.""" + return self.shortcuts[row_num] + + def reset(self): + """"Reset model to take into account new search letters.""" + self.beginResetModel() + self.endResetModel() + -class ShortcutsDelegate(QItemDelegate): +class CustomSortFilterProxy(QSortFilterProxyModel): + """Custom column filter based on regex.""" def __init__(self, parent=None): - QItemDelegate.__init__(self, parent) - self.modifiers = sorted(Key.MODIFIERNAMES.values()) - self.mod = None - self.keys = sorted(Key.KEYS.values()) - self.key = None - - def sizeHint(self, option, index): - fm = option.fontMetrics - if index.column() in (MOD1, MOD2, MOD3): - if self.mod is None: - w = 0 - for mod in self.modifiers: - cw = fm.width(mod) - if cw > w: - w = cw - self.mod = mod - else: - w = fm.width(self.mod) - return QSize(w+20, fm.height()) - elif index.column() == KEY: - if self.key is None: - w = 0 - for key in self.keys: - cw = fm.width(key) - if cw > w: - w = cw - self.key = key - else: - w = fm.width(self.key) - return QSize(w+20, fm.height()) - return QItemDelegate.sizeHint(self, option, index) - - def createEditor(self, parent, option, index): - if index.column() in (MOD1, MOD2, MOD3): - combobox = QComboBox(parent) - combobox.addItems(self.modifiers) - return combobox - elif index.column() == KEY: - combobox = QComboBox(parent) - combobox.addItems(self.keys) - return combobox + super(CustomSortFilterProxy, self).__init__(parent) + self._parent = parent + self.pattern = re.compile(u'') + + def set_filter(self, text): + """Set regular expression for filter.""" + self.pattern = get_search_regex(text) + if self.pattern: + self._parent.setSortingEnabled(False) else: - return QItemDelegate.createEditor(self, parent, option, - index) + self._parent.setSortingEnabled(True) + self.invalidateFilter() - def setEditorData(self, editor, index): - text = from_qvariant(index.model().data(index, Qt.DisplayRole), str) - if index.column() in (MOD1, MOD2, MOD3, KEY): - i = editor.findText(text) - if i == -1: - i = 0 - editor.setCurrentIndex(i) - else: - QItemDelegate.setEditorData(self, editor, index) + def filterAcceptsRow(self, row_num, parent): + """Qt override. + + Reimplemented from base class to allow the use of custom filtering. + """ + model = self.sourceModel() + name = model.row(row_num).name + r = re.search(self.pattern, name) - def setModelData(self, editor, model, index): - if index.column() in (MOD1, MOD2, MOD3, KEY): - model.setData(index, to_qvariant(editor.currentText())) + if r is None: + return False else: - QItemDelegate.setModelData(self, editor, model, index) + return True class ShortcutsTable(QTableView): def __init__(self, parent=None): QTableView.__init__(self, parent) - self.model = ShortcutsModel() - self.setModel(self.model) - self.setItemDelegate(ShortcutsDelegate(self)) + self._parent = parent + self.finder = None + + self.source_model = ShortcutsModel(self) + self.proxy_model = CustomSortFilterProxy(self) + self.last_regex = '' + + self.proxy_model.setSourceModel(self.source_model) + self.proxy_model.setDynamicSortFilter(True) + self.proxy_model.setFilterKeyColumn(NAME) + self.proxy_model.setFilterCaseSensitivity(Qt.CaseInsensitive) + self.setModel(self.proxy_model) + + self.hideColumn(SEARCH_SCORE) + self.setItemDelegateForColumn(NAME, HTMLDelegate(self, margin=9)) + self.setSelectionBehavior(QAbstractItemView.SelectRows) + self.setSelectionMode(QAbstractItemView.SingleSelection) + self.setSortingEnabled(True) + self.setEditTriggers(QAbstractItemView.AllEditTriggers) + self.selectionModel().selectionChanged.connect(self.selection) + + self.verticalHeader().hide() self.load_shortcuts() - + + def focusOutEvent(self, e): + """Qt Override.""" + self.source_model.update_active_row() + super(ShortcutsTable, self).focusOutEvent(e) + + def focusInEvent(self, e): + """Qt Override.""" + super(ShortcutsTable, self).focusInEvent(e) + self.selectRow(self.currentIndex().row()) + + def selection(self, index): + """Update selected row.""" + self.update() + self.isActiveWindow() + def adjust_cells(self): + """Adjust column size based on contents.""" self.resizeColumnsToContents() -# self.resizeRowsToContents() + fm = self.horizontalHeader().fontMetrics() + names = [fm.width(s.name + ' '*9) for s in self.source_model.shortcuts] + self.setColumnWidth(NAME, max(names)) self.horizontalHeader().setStretchLastSection(True) - + def load_shortcuts(self): + """Load shortcuts and assign to table model.""" shortcuts = [] for context, name, keystr in iter_shortcuts(): shortcut = Shortcut(context, name, keystr) shortcuts.append(shortcut) shortcuts = sorted(shortcuts, key=lambda x: x.context+x.name) - self.model.shortcuts = shortcuts - self.model.reset() + # Store the original order of shortcuts + for i, shortcut in enumerate(shortcuts): + shortcut.index = i + self.source_model.shortcuts = shortcuts + self.source_model.scores = [0]*len(shortcuts) + self.source_model.rich_text = [s.name for s in shortcuts] + self.source_model.reset() self.adjust_cells() + self.sortByColumn(CONTEXT, Qt.AscendingOrder) def check_shortcuts(self): - """Check shortcuts for conflicts""" + """Check shortcuts for conflicts.""" conflicts = [] - for index, sh1 in enumerate(self.model.shortcuts): - if index == len(self.model.shortcuts)-1: + for index, sh1 in enumerate(self.source_model.shortcuts): + if index == len(self.source_model.shortcuts)-1: break - for sh2 in self.model.shortcuts[index+1:]: + for sh2 in self.source_model.shortcuts[index+1:]: if sh2 is sh1: continue if str(sh2.key) == str(sh1.key) \ - and (sh1.context == sh2.context or sh1.context == '_' - or sh2.context == '_'): + and (sh1.context == sh2.context or sh1.context == '_' or + sh2.context == '_'): conflicts.append((sh1, sh2)) if conflicts: - self.parent().emit(SIGNAL('show_this_page()')) + self.parent().show_this_page.emit() cstr = "\n".join(['%s <---> %s' % (sh1, sh2) for sh1, sh2 in conflicts]) - QMessageBox.warning(self, _( "Conflicts"), + QMessageBox.warning(self, _("Conflicts"), _("The following conflicts have been " "detected:")+"\n"+cstr, QMessageBox.Ok) - + def save_shortcuts(self): + """Save shortcuts from table model.""" self.check_shortcuts() - for shortcut in self.model.shortcuts: + for shortcut in self.source_model.shortcuts: shortcut.save() - + + def show_editor(self): + """Create, setup and display the shortcut editor dialog.""" + index = self.proxy_model.mapToSource(self.currentIndex()) + row, column = index.row(), index.column() + shortcuts = self.source_model.shortcuts + context = shortcuts[row].context + name = shortcuts[row].name + + sequence_index = self.source_model.index(row, SEQUENCE) + sequence = sequence_index.data() + + dialog = ShortcutEditor(self, context, name, sequence, shortcuts) + + if dialog.exec_(): + new_sequence = dialog.new_sequence + self.source_model.setData(sequence_index, new_sequence) + + def set_regex(self, regex=None, reset=False): + """Update the regex text for the shortcut finder.""" + if reset: + text = '' + else: + text = self.finder.text().replace(' ', '').lower() + + self.proxy_model.set_filter(text) + self.source_model.update_search_letters(text) + self.sortByColumn(SEARCH_SCORE, Qt.AscendingOrder) + + if self.last_regex != regex: + self.selectRow(0) + self.last_regex = regex + + def next_row(self): + """Move to next row from currently selected row.""" + row = self.currentIndex().row() + rows = self.proxy_model.rowCount() + if row + 1 == rows: + row = -1 + self.selectRow(row + 1) + + def previous_row(self): + """Move to previous row from currently selected row.""" + row = self.currentIndex().row() + rows = self.proxy_model.rowCount() + if row == 0: + row = rows + self.selectRow(row - 1) + + def keyPressEvent(self, event): + """Qt Override.""" + key = event.key() + if key in [Qt.Key_Enter, Qt.Key_Return]: + self.show_editor() + elif key in [Qt.Key_Tab]: + self.finder.setFocus() + elif key in [Qt.Key_Backtab]: + self.parent().reset_btn.setFocus() + elif key in [Qt.Key_Up, Qt.Key_Down, Qt.Key_Left, Qt.Key_Right]: + super(ShortcutsTable, self).keyPressEvent(event) + elif key not in [Qt.Key_Escape, Qt.Key_Space]: + text = event.text() + if text: + if re.search(VALID_FINDER_CHARS, text) is not None: + self.finder.setFocus() + self.finder.set_text(text) + elif key in [Qt.Key_Escape]: + self.finder.keyPressEvent(event) + + def mouseDoubleClickEvent(self, event): + """Qt Override.""" + self.show_editor() + class ShortcutsConfigPage(GeneralConfigPage): CONF_SECTION = "shortcuts" - + NAME = _("Keyboard shortcuts") - ICON = "genprefs.png" - + ICON = ima.icon('keyboard') + def setup_page(self): + # Widgets self.table = ShortcutsTable(self) - self.connect(self.table.model, - SIGNAL("dataChanged(QModelIndex,QModelIndex)"), - lambda i1, i2, opt='': self.has_been_modified(opt)) + self.finder = ShortcutFinder(self.table, self.table.set_regex) + self.table.finder = self.finder + self.label_finder = QLabel(_('Search: ')) + self.reset_btn = QPushButton(_("Reset to default values")) + + # Layout + hlayout = QHBoxLayout() vlayout = QVBoxLayout() + hlayout.addWidget(self.label_finder) + hlayout.addWidget(self.finder) vlayout.addWidget(self.table) - reset_btn = QPushButton(_("Reset to default values")) - self.connect(reset_btn, SIGNAL('clicked()'), self.reset_to_default) - vlayout.addWidget(reset_btn) + vlayout.addLayout(hlayout) + vlayout.addWidget(self.reset_btn) self.setLayout(vlayout) - + + self.setTabOrder(self.table, self.finder) + self.setTabOrder(self.finder, self.reset_btn) + + # Signals and slots + if PYQT5: + # Qt5 'dataChanged' has 3 parameters + self.table.proxy_model.dataChanged.connect( + lambda i1, i2, roles, opt='': self.has_been_modified(opt)) + else: + self.table.proxy_model.dataChanged.connect( + lambda i1, i2, opt='': self.has_been_modified(opt)) + self.reset_btn.clicked.connect(self.reset_to_default) + def check_settings(self): self.table.check_shortcuts() - + def reset_to_default(self): reset_shortcuts() self.main.apply_shortcuts() self.table.load_shortcuts() self.load_from_conf() self.set_modified(False) - + def apply_settings(self, options): self.table.save_shortcuts() self.main.apply_shortcuts() def test(): - from spyderlib.utils.qthelpers import qapplication + from spyder.utils.qthelpers import qapplication app = qapplication() table = ShortcutsTable() table.show() app.exec_() - print([str(s) for s in table.model.shortcuts]) + print([str(s) for s in table.source_model.shortcuts]) table.check_shortcuts() if __name__ == '__main__': - test() \ Kein Zeilenumbruch am Dateiende. + test() diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/plugins/variableexplorer.py spyder-3.0.2+dfsg1/spyder/plugins/variableexplorer.py --- spyder-2.3.8+dfsg1/spyder/plugins/variableexplorer.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/plugins/variableexplorer.py 2016-11-19 01:31:58.000000000 +0100 @@ -1,22 +1,24 @@ # -*- coding: utf-8 -*- # -# Copyright © 2009-2010 Pierre Raybaut +# Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) +# (see spyder/__init__.py for details) -"""Namespace Browser Plugin""" +"""Variable Explorer Plugin""" -from spyderlib.qt.QtGui import QStackedWidget, QGroupBox, QVBoxLayout -from spyderlib.qt.QtCore import Signal +# Third party imports +from qtpy.QtCore import Signal +from qtpy.QtWidgets import QGroupBox, QStackedWidget, QVBoxLayout, QWidget # Local imports -from spyderlib.baseconfig import _ -from spyderlib.config import CONF -from spyderlib.utils.qthelpers import get_icon -from spyderlib.utils import programs -from spyderlib.plugins import SpyderPluginMixin, PluginConfigPage -from spyderlib.widgets.externalshell.monitor import REMOTE_SETTINGS -from spyderlib.widgets.externalshell.namespacebrowser import NamespaceBrowser +from spyder.config.base import _ +from spyder.config.main import CONF +from spyder.plugins import SpyderPluginMixin +from spyder.plugins.configdialog import PluginConfigPage +from spyder.utils import programs +from spyder.utils import icon_manager as ima +from spyder.widgets.variableexplorer.namespacebrowser import NamespaceBrowser +from spyder.widgets.variableexplorer.utils import REMOTE_SETTINGS class VariableExplorerConfigPage(PluginConfigPage): @@ -39,7 +41,7 @@ for option, text in filter_data] display_group = QGroupBox(_("Display")) - display_data = [('truncate', _("Truncate values"), '')] + display_data = [] if programs.is_module_installed('numpy'): display_data.append(('minmax', _("Show arrays min/max"), '')) display_data.append( @@ -75,18 +77,27 @@ self.setLayout(vlayout) -class VariableExplorer(QStackedWidget, SpyderPluginMixin): +class VariableExplorer(QWidget, SpyderPluginMixin): """ Variable Explorer Plugin """ CONF_SECTION = 'variable_explorer' CONFIGWIDGET_CLASS = VariableExplorerConfigPage sig_option_changed = Signal(str, object) + def __init__(self, parent): - QStackedWidget.__init__(self, parent) + QWidget.__init__(self, parent) SpyderPluginMixin.__init__(self, parent) + + # Widgets + self.stack = QStackedWidget(self) self.shellwidgets = {} + # Layout + layout = QVBoxLayout() + layout.addWidget(self.stack) + self.setLayout(layout) + # Initialize plugin self.initialize_plugin() @@ -101,13 +112,29 @@ for name in REMOTE_SETTINGS: settings[name] = CONF.get(VariableExplorer.CONF_SECTION, name) return settings - - #------ Public API --------------------------------------------------------- + + # ----- Stack accesors ---------------------------------------------------- + def set_current_widget(self, nsb): + self.stack.setCurrentWidget(nsb) + + def current_widget(self): + return self.stack.currentWidget() + + def count(self): + return self.stack.count() + + def remove_widget(self, nsb): + self.stack.removeWidget(nsb) + + def add_widget(self, nsb): + self.stack.addWidget(nsb) + + # ----- Public API -------------------------------------------------------- def add_shellwidget(self, shellwidget): shellwidget_id = id(shellwidget) - # Add shell only once: this method may be called two times in a row + # Add shell only once: this method may be called two times in a row # by the External console plugin (dev. convenience) - from spyderlib.widgets.externalshell import systemshell + from spyder.widgets.externalshell import systemshell if isinstance(shellwidget, systemshell.ExternalSystemShell): return if shellwidget_id not in self.shellwidgets: @@ -115,7 +142,7 @@ nsb.set_shellwidget(shellwidget) nsb.setup(**VariableExplorer.get_settings()) nsb.sig_option_changed.connect(self.sig_option_changed.emit) - self.addWidget(nsb) + self.add_widget(nsb) self.shellwidgets[shellwidget_id] = nsb self.set_shellwidget_from_id(shellwidget_id) return nsb @@ -125,22 +152,22 @@ # that shell was not a Python-based console (it was a terminal) if shellwidget_id in self.shellwidgets: nsb = self.shellwidgets.pop(shellwidget_id) - self.removeWidget(nsb) + self.remove_widget(nsb) nsb.close() def set_shellwidget_from_id(self, shellwidget_id): if shellwidget_id in self.shellwidgets: nsb = self.shellwidgets[shellwidget_id] - self.setCurrentWidget(nsb) + self.set_current_widget(nsb) if self.isvisible: nsb.visibility_changed(True) - + def import_data(self, fname): """Import data in current namespace""" if self.count(): - nsb = self.currentWidget() + nsb = self.current_widget() nsb.refresh_table() - nsb.import_data(fname) + nsb.import_data(filenames=fname) if self.dockwidget and not self.ismaximized: self.dockwidget.setVisible(True) self.dockwidget.raise_() @@ -150,7 +177,7 @@ """DockWidget visibility has changed""" SpyderPluginMixin.visibility_changed(self, enable) for nsb in list(self.shellwidgets.values()): - nsb.visibility_changed(enable and nsb is self.currentWidget()) + nsb.visibility_changed(enable and nsb is self.current_widget()) #------ SpyderPluginWidget API --------------------------------------------- def get_plugin_title(self): @@ -159,14 +186,14 @@ def get_plugin_icon(self): """Return plugin icon""" - return get_icon('dictedit.png') + return ima.icon('dictedit') def get_focus_widget(self): """ Return the widget to give focus to when this plugin's dockwidget is raised on top-level """ - return self.currentWidget() + return self.current_widget() def closing_plugin(self, cancelable=False): """Perform actions before parent main window is closed""" @@ -191,4 +218,4 @@ nsb.setup(**VariableExplorer.get_settings()) ar_timeout = self.get_option('autorefresh/timeout') for shellwidget in self.main.extconsole.shellwidgets: - shellwidget.set_autorefresh_timeout(ar_timeout) \ Kein Zeilenumbruch am Dateiende. + shellwidget.set_autorefresh_timeout(ar_timeout) diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/plugins/workingdirectory.py spyder-3.0.2+dfsg1/spyder/plugins/workingdirectory.py --- spyder-2.3.8+dfsg1/spyder/plugins/workingdirectory.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/plugins/workingdirectory.py 2016-11-19 01:31:58.000000000 +0100 @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- # -# Copyright © 2009-2010 Pierre Raybaut +# Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) +# (see spyder/__init__.py for details) """Working Directory Plugin""" @@ -11,23 +11,26 @@ # pylint: disable=R0911 # pylint: disable=R0201 -from spyderlib.qt.QtGui import (QToolBar, QLabel, QGroupBox, QVBoxLayout, - QHBoxLayout, QButtonGroup) -from spyderlib.qt.QtCore import SIGNAL, Signal -from spyderlib.qt.compat import getexistingdirectory - +# Standard library imports import os import os.path as osp +# Third party imports +from qtpy import PYQT5 +from qtpy.compat import getexistingdirectory +from qtpy.QtCore import QSize, Signal, Slot +from qtpy.QtWidgets import (QButtonGroup, QGroupBox, QHBoxLayout, QLabel, + QToolBar, QVBoxLayout) + # Local imports -from spyderlib.utils import encoding -from spyderlib.baseconfig import get_conf_path, _ -from spyderlib.utils.qthelpers import get_icon, get_std_icon, create_action - -# Package local imports -from spyderlib.widgets.comboboxes import PathComboBox -from spyderlib.plugins import SpyderPluginMixin, PluginConfigPage -from spyderlib.py3compat import to_text_string, getcwd +from spyder.config.base import _, get_conf_path, get_home_dir +from spyder.plugins import SpyderPluginMixin +from spyder.plugins.configdialog import PluginConfigPage +from spyder.py3compat import to_text_string, getcwd +from spyder.utils import encoding +from spyder.utils import icon_manager as ima +from spyder.utils.qthelpers import create_action +from spyder.widgets.comboboxes import PathComboBox class WorkingDirectoryConfigPage(PluginConfigPage): @@ -58,10 +61,8 @@ button_group=startup_bg) thisdir_bd = self.create_browsedir("", 'startup/fixed_directory', getcwd()) - self.connect(thisdir_radio, SIGNAL("toggled(bool)"), - thisdir_bd.setEnabled) - self.connect(lastdir_radio, SIGNAL("toggled(bool)"), - thisdir_bd.setDisabled) + thisdir_radio.toggled.connect(thisdir_bd.setEnabled) + lastdir_radio.toggled.connect(thisdir_bd.setDisabled) thisdir_layout = QHBoxLayout() thisdir_layout.addWidget(thisdir_radio) thisdir_layout.addWidget(thisdir_bd) @@ -142,10 +143,21 @@ CONF_SECTION = 'workingdir' CONFIGWIDGET_CLASS = WorkingDirectoryConfigPage LOG_PATH = get_conf_path(CONF_SECTION) + sig_option_changed = Signal(str, object) - def __init__(self, parent, workdir=None): - QToolBar.__init__(self, parent) - SpyderPluginMixin.__init__(self, parent) + set_previous_enabled = Signal(bool) + set_next_enabled = Signal(bool) + redirect_stdio = Signal(bool) + set_explorer_cwd = Signal(str) + refresh_findinfiles = Signal() + set_current_console_wd = Signal(str) + + def __init__(self, parent, workdir=None, **kwds): + if PYQT5: + super(WorkingDirectory, self).__init__(parent, **kwds) + else: + QToolBar.__init__(self, parent) + SpyderPluginMixin.__init__(self, parent) # Initialize plugin self.initialize_plugin() @@ -157,7 +169,7 @@ self.history = [] self.histindex = None self.previous_action = create_action(self, "previous", None, - get_icon('previous.png'), _('Back'), + ima.icon('previous'), _('Back'), triggered=self.previous_directory) self.addAction(self.previous_action) @@ -165,15 +177,13 @@ self.history = [] self.histindex = None self.next_action = create_action(self, "next", None, - get_icon('next.png'), _('Next'), + ima.icon('next'), _('Next'), triggered=self.next_directory) self.addAction(self.next_action) # Enable/disable previous/next actions - self.connect(self, SIGNAL("set_previous_enabled(bool)"), - self.previous_action.setEnabled) - self.connect(self, SIGNAL("set_next_enabled(bool)"), - self.next_action.setEnabled) + self.set_previous_enabled.connect(self.previous_action.setEnabled) + self.set_next_enabled.connect(self.next_action.setEnabled) # Path combo box adjust = self.get_option('working_dir_adjusttocontents') @@ -183,9 +193,10 @@ "terminals), for the file explorer, for the\n" "find in files plugin and for new files\n" "created in the editor")) - self.connect(self.pathedit, SIGNAL("open_dir(QString)"), self.chdir) + self.pathedit.open_dir.connect(self.chdir) + self.pathedit.activated[str].connect(self.chdir) self.pathedit.setMaxCount(self.get_option('working_dir_history')) - wdhistory = self.load_wdhistory( workdir ) + wdhistory = self.load_wdhistory(workdir) if workdir is None: if self.get_option('startup/use_last_directory'): if wdhistory: @@ -197,27 +208,21 @@ if not osp.isdir(workdir): workdir = "." self.chdir(workdir) - self.pathedit.addItems( wdhistory ) + self.pathedit.addItems(wdhistory) + self.pathedit.selected_text = self.pathedit.currentText() self.refresh_plugin() self.addWidget(self.pathedit) # Browse action browse_action = create_action(self, "browse", None, - get_std_icon('DirOpenIcon'), + ima.icon('DirOpenIcon'), _('Browse a working directory'), triggered=self.select_directory) self.addAction(browse_action) - - # Set current console working directory action - setwd_action = create_action(self, icon=get_icon('set_workdir.png'), - text=_("Set as current console's " - "working directory"), - triggered=self.set_as_current_console_wd) - self.addAction(setwd_action) - + # Parent dir action parent_action = create_action(self, "parent", None, - get_icon('up.png'), + ima.icon('up'), _('Change to parent directory'), triggered=self.parent_directory) self.addAction(parent_action) @@ -229,7 +234,7 @@ def get_plugin_icon(self): """Return widget icon""" - return get_std_icon('DirOpenIcon') + return ima.icon('DirOpenIcon') def get_plugin_actions(self): """Setup actions""" @@ -237,10 +242,10 @@ def register_plugin(self): """Register plugin in Spyder's main window""" - self.connect(self, SIGNAL('redirect_stdio(bool)'), - self.main.redirect_internalshell_stdio) - self.connect(self.main.console.shell, SIGNAL("refresh()"), - self.refresh_plugin) + self.redirect_stdio.connect(self.main.redirect_internalshell_stdio) + self.main.console.shell.refresh.connect(self.refresh_plugin) + iconsize = 24 + self.setIconSize(QSize(iconsize, iconsize)) self.main.addToolBar(self) def refresh_plugin(self): @@ -248,11 +253,10 @@ curdir = getcwd() self.pathedit.add_text(curdir) self.save_wdhistory() - self.emit(SIGNAL("set_previous_enabled(bool)"), - self.histindex is not None and self.histindex > 0) - self.emit(SIGNAL("set_next_enabled(bool)"), - self.histindex is not None and \ - self.histindex < len(self.history)-1) + self.set_previous_enabled.emit( + self.histindex is not None and self.histindex > 0) + self.set_next_enabled.emit(self.histindex is not None and \ + self.histindex < len(self.history)-1) def apply_plugin_settings(self, options): """Apply configuration file's plugin settings""" @@ -270,45 +274,53 @@ wdhistory = [name for name in wdhistory if os.path.isdir(name)] else: if workdir is None: - workdir = getcwd() + workdir = get_home_dir() wdhistory = [ workdir ] return wdhistory - + def save_wdhistory(self): """Save history to a text file in user home directory""" text = [ to_text_string( self.pathedit.itemText(index) ) \ for index in range(self.pathedit.count()) ] encoding.writelines(text, self.LOG_PATH) - + + @Slot() def select_directory(self): """Select directory""" - self.emit(SIGNAL('redirect_stdio(bool)'), False) + self.redirect_stdio.emit(False) directory = getexistingdirectory(self.main, _("Select directory"), getcwd()) if directory: self.chdir(directory) - self.emit(SIGNAL('redirect_stdio(bool)'), True) - + self.redirect_stdio.emit(True) + + @Slot() def previous_directory(self): """Back to previous directory""" self.histindex -= 1 - self.chdir(browsing_history=True) - + self.chdir(directory='', browsing_history=True) + + @Slot() def next_directory(self): """Return to next directory""" self.histindex += 1 - self.chdir(browsing_history=True) - + self.chdir(directory='', browsing_history=True) + + @Slot() def parent_directory(self): """Change working directory to parent directory""" self.chdir(os.path.join(getcwd(), os.path.pardir)) - - def chdir(self, directory=None, browsing_history=False, + + @Slot(str) + @Slot(str, bool) + @Slot(str, bool, bool) + def chdir(self, directory, browsing_history=False, refresh_explorer=True): """Set directory as working directory""" - # Working directory history management - if directory is not None: + if directory: directory = osp.abspath(to_text_string(directory)) + + # Working directory history management if browsing_history: directory = self.history[self.histindex] elif directory in self.history: @@ -325,9 +337,10 @@ os.chdir( to_text_string(directory) ) self.refresh_plugin() if refresh_explorer: - self.emit(SIGNAL("set_explorer_cwd(QString)"), directory) - self.emit(SIGNAL("refresh_findinfiles()")) - + self.set_explorer_cwd.emit(directory) + self.set_as_current_console_wd() + self.refresh_findinfiles.emit() + def set_as_current_console_wd(self): """Set as current console working directory""" - self.emit(SIGNAL("set_current_console_wd(QString)"), getcwd()) + self.set_current_console_wd.emit(getcwd()) diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/py3compat.py spyder-3.0.2+dfsg1/spyder/py3compat.py --- spyder-2.3.8+dfsg1/spyder/py3compat.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/py3compat.py 2016-10-25 02:05:23.000000000 +0200 @@ -1,14 +1,14 @@ # -*- coding: utf-8 -*- # -# Copyright © 2012-2013 Pierre Raybaut +# Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) +# (see spyder/__init__.py for details) """ -spyderlib.py3compat -------------------- +spyder.py3compat +---------------- -Transitional module providing compatibility functions intended to help +Transitional module providing compatibility functions intended to help migrating from Python 2 to Python 3. This module should be fully compatible with: @@ -18,13 +18,13 @@ from __future__ import print_function -import sys +import operator import os +import sys PY2 = sys.version[0] == '2' PY3 = sys.version[0] == '3' - #============================================================================== # Data types #============================================================================== @@ -62,6 +62,7 @@ from UserDict import DictMixin as MutableMapping import thread as _thread import repr as reprlib + import Queue else: # Python 3 import builtins @@ -76,7 +77,7 @@ from collections import MutableMapping import _thread import reprlib - + import queue as Queue #============================================================================== # Strings @@ -243,6 +244,46 @@ """Convert QByteArray object to str in a way compatible with Python 2/3""" return str(bytes(qba.toHex().data()).decode()) +# ============================================================================= +# Dict funcs +# ============================================================================= +if PY3: + def iterkeys(d, **kw): + return iter(d.keys(**kw)) + + def itervalues(d, **kw): + return iter(d.values(**kw)) + + def iteritems(d, **kw): + return iter(d.items(**kw)) + + def iterlists(d, **kw): + return iter(d.lists(**kw)) + + viewkeys = operator.methodcaller("keys") + + viewvalues = operator.methodcaller("values") + + viewitems = operator.methodcaller("items") +else: + def iterkeys(d, **kw): + return d.iterkeys(**kw) + + def itervalues(d, **kw): + return d.itervalues(**kw) + + def iteritems(d, **kw): + return d.iteritems(**kw) + + def iterlists(d, **kw): + return d.iterlists(**kw) + + viewkeys = operator.methodcaller("viewkeys") + + viewvalues = operator.methodcaller("viewvalues") + + viewitems = operator.methodcaller("viewitems") + if __name__ == '__main__': pass diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/pygments_patch.py spyder-3.0.2+dfsg1/spyder/pygments_patch.py --- spyder-2.3.8+dfsg1/spyder/pygments_patch.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/pygments_patch.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,81 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright © 2014 The Spyder development team -# Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) - -""" -Patching pygments to avoid errors with IPython console -""" - -def apply(): - """ - Monkey patching pygments - See Issue 2042 and https://github.com/ipython/ipython/pull/6878 - """ - from spyderlib.utils.programs import is_module_installed - if is_module_installed('pygments', '<2.0') and \ - is_module_installed('IPython', '>3.0'): - return - - # Patching IPython's patch of RegLexer (Oh God!!) - from pygments.lexer import _TokenType, Text, Error - from spyderlib.py3compat import to_text_string - try: - from IPython.qt.console.pygments_highlighter import RegexLexer - except ImportError: - from IPython.frontend.qt.console.pygments_highlighter import RegexLexer - def get_tokens_unprocessed(self, text, stack=('root',)): - pos = 0 - tokendefs = self._tokens - if hasattr(self, '_saved_state_stack'): - statestack = list(self._saved_state_stack) - else: - statestack = list(stack) - statetokens = tokendefs[statestack[-1]] - while 1: - for rexmatch, action, new_state in statetokens: - m = rexmatch(text, pos) - if m: - if action is not None: - if type(action) is _TokenType: - yield pos, action, m.group() - else: - for item in action(self, m): - yield item - pos = m.end() - if new_state is not None: - # state transition - if isinstance(new_state, tuple): - for state in new_state: - if state == '#pop': - statestack.pop() - elif state == '#push': - statestack.append(statestack[-1]) - else: - statestack.append(state) - elif isinstance(new_state, int): - # pop - del statestack[new_state:] - elif new_state == '#push': - statestack.append(statestack[-1]) - else: - assert False, "wrong state def: %r" % new_state - statetokens = tokendefs[statestack[-1]] - break - else: - try: - if text[pos] == '\n': - # at EOL, reset state to "root" - pos += 1 - statestack = ['root'] - statetokens = tokendefs['root'] - yield pos, Text, to_text_string('\n') - continue - yield pos, Error, text[pos] - pos += 1 - except IndexError: - break - self._saved_state_stack = list(statestack) - - RegexLexer.get_tokens_unprocessed = get_tokens_unprocessed diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/pyplot.py spyder-3.0.2+dfsg1/spyder/pyplot.py --- spyder-2.3.8+dfsg1/spyder/pyplot.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/pyplot.py 2016-10-25 02:05:23.000000000 +0200 @@ -5,5 +5,5 @@ try: from guiqwt.pyplot import * -except ImportError: +except (ImportError, AssertionError): from matplotlib.pyplot import * diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/qt/compat.py spyder-3.0.2+dfsg1/spyder/qt/compat.py --- spyder-2.3.8+dfsg1/spyder/qt/compat.py 2015-08-24 21:09:44.000000000 +0200 +++ spyder-3.0.2+dfsg1/spyder/qt/compat.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,210 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright © 2011-2012 Pierre Raybaut -# Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) - -""" -spyderlib.qt.compat -------------------- - -Transitional module providing compatibility functions intended to help -migrating from PyQt to PySide. - -This module should be fully compatible with: - * PyQt >=v4.4 - * both PyQt API #1 and API #2 - * PySide -""" - -from __future__ import print_function - -import os -import sys -import collections - -from spyderlib.qt.QtGui import QFileDialog - -from spyderlib.py3compat import is_text_string, to_text_string, TEXT_TYPES - -#============================================================================== -# QVariant conversion utilities -#============================================================================== - -PYQT_API_1 = False -if os.environ['QT_API'] == 'pyqt': - import sip - try: - PYQT_API_1 = sip.getapi('QVariant') == 1 # PyQt API #1 - except AttributeError: - # PyQt =v4.4 (API #1 and #2) and PySide >=v1.0""" - # Calling QFileDialog static method - if sys.platform == "win32": - # On Windows platforms: redirect standard outputs - _temp1, _temp2 = sys.stdout, sys.stderr - sys.stdout, sys.stderr = None, None - try: - result = QFileDialog.getExistingDirectory(parent, caption, basedir, - options) - finally: - if sys.platform == "win32": - # On Windows platforms: restore standard outputs - sys.stdout, sys.stderr = _temp1, _temp2 - if not is_text_string(result): - # PyQt API #1 - result = to_text_string(result) - return result - -def _qfiledialog_wrapper(attr, parent=None, caption='', basedir='', - filters='', selectedfilter='', options=None): - if options is None: - options = QFileDialog.Options(0) - try: - # PyQt =v4.6 - QString = None # analysis:ignore - tuple_returned = True - try: - # PyQt >=v4.6 - func = getattr(QFileDialog, attr+'AndFilter') - except AttributeError: - # PySide or PyQt =v4.6 - output, selectedfilter = result - else: - # PyQt =v4.4 (API #1 and #2) and PySide >=v1.0""" - return _qfiledialog_wrapper('getOpenFileName', parent=parent, - caption=caption, basedir=basedir, - filters=filters, selectedfilter=selectedfilter, - options=options) - -def getopenfilenames(parent=None, caption='', basedir='', filters='', - selectedfilter='', options=None): - """Wrapper around QtGui.QFileDialog.getOpenFileNames static method - Returns a tuple (filenames, selectedfilter) -- when dialog box is canceled, - returns a tuple (empty list, empty string) - Compatible with PyQt >=v4.4 (API #1 and #2) and PySide >=v1.0""" - return _qfiledialog_wrapper('getOpenFileNames', parent=parent, - caption=caption, basedir=basedir, - filters=filters, selectedfilter=selectedfilter, - options=options) - -def getsavefilename(parent=None, caption='', basedir='', filters='', - selectedfilter='', options=None): - """Wrapper around QtGui.QFileDialog.getSaveFileName static method - Returns a tuple (filename, selectedfilter) -- when dialog box is canceled, - returns a tuple of empty strings - Compatible with PyQt >=v4.4 (API #1 and #2) and PySide >=v1.0""" - return _qfiledialog_wrapper('getSaveFileName', parent=parent, - caption=caption, basedir=basedir, - filters=filters, selectedfilter=selectedfilter, - options=options) - -if __name__ == '__main__': - from spyderlib.utils.qthelpers import qapplication - _app = qapplication() - print(repr(getexistingdirectory())) - print(repr(getopenfilename(filters='*.py;;*.txt'))) - print(repr(getopenfilenames(filters='*.py;;*.txt'))) - print(repr(getsavefilename(filters='*.py;;*.txt'))) - sys.exit() diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/qt/__init__.py spyder-3.0.2+dfsg1/spyder/qt/__init__.py --- spyder-2.3.8+dfsg1/spyder/qt/__init__.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/qt/__init__.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,52 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright © 2011 Pierre Raybaut -# Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) - -"""Transitional package (PyQt4 --> PySide)""" - -import os - -os.environ.setdefault('QT_API', 'pyqt') -assert os.environ['QT_API'] in ('pyqt', 'pyside') - -API = os.environ['QT_API'] -API_NAME = {'pyqt': 'PyQt4', 'pyside': 'PySide'}[API] - -if API == 'pyqt': - # Since Spyder 2.3.6 we only support API #2 - try: - import sip - try: - sip.setapi('QString', 2) - sip.setapi('QVariant', 2) - sip.setapi('QDate', 2) - sip.setapi('QDateTime', 2) - sip.setapi('QTextStream', 2) - sip.setapi('QTime', 2) - sip.setapi('QUrl', 2) - except AttributeError: - pass - - from PyQt4.QtCore import PYQT_VERSION_STR as __version__ - except ImportError: # May fail on sip or on PyQt4 import - # Switching to PySide - API = os.environ['QT_API'] = 'pyside' - API_NAME = 'PySide' - else: - is_old_pyqt = __version__.startswith(('4.4', '4.5', '4.6', '4.7')) - is_pyqt46 = __version__.startswith('4.6') - import sip - try: - API_NAME += (" (API v%d)" % sip.getapi('QString')) - except AttributeError: - pass - -if API == 'pyside': - try: - from PySide import __version__ # analysis:ignore - except ImportError: - raise ImportError("Spyder requires PySide or PyQt to be installed") - else: - is_old_pyqt = is_pyqt46 = False diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/qt/QtCore.py spyder-3.0.2+dfsg1/spyder/qt/QtCore.py --- spyder-2.3.8+dfsg1/spyder/qt/QtCore.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/qt/QtCore.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright © 2011 Pierre Raybaut -# Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) - -import os - -if os.environ['QT_API'] == 'pyqt': - from PyQt4.QtCore import * # analysis:ignore - from PyQt4.QtCore import QCoreApplication # analysis:ignore - from PyQt4.QtCore import Qt # analysis:ignore - from PyQt4.QtCore import pyqtSignal as Signal # analysis:ignore - from PyQt4.QtCore import pyqtSlot as Slot # analysis:ignore - from PyQt4.QtCore import pyqtProperty as Property # analysis:ignore - from PyQt4.QtCore import QT_VERSION_STR as __version__ -else: - import PySide.QtCore - __version__ = PySide.QtCore.__version__ # analysis:ignore - from PySide.QtCore import * # analysis:ignore diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/qt/QtGui.py spyder-3.0.2+dfsg1/spyder/qt/QtGui.py --- spyder-2.3.8+dfsg1/spyder/qt/QtGui.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/qt/QtGui.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,13 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright © 2011 Pierre Raybaut -# Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) - -import os - -if os.environ['QT_API'] == 'pyqt': - from PyQt4.Qt import QKeySequence, QTextCursor # analysis:ignore - from PyQt4.QtGui import * # analysis:ignore -else: - from PySide.QtGui import * # analysis:ignore diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/qt/QtSvg.py spyder-3.0.2+dfsg1/spyder/qt/QtSvg.py --- spyder-2.3.8+dfsg1/spyder/qt/QtSvg.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/qt/QtSvg.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,12 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright © 2012 Pierre Raybaut -# Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) - -import os - -if os.environ['QT_API'] == 'pyqt': - from PyQt4.QtSvg import * # analysis:ignore -else: - from PySide.QtSvg import * # analysis:ignore \ Kein Zeilenumbruch am Dateiende. diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/qt/QtWebKit.py spyder-3.0.2+dfsg1/spyder/qt/QtWebKit.py --- spyder-2.3.8+dfsg1/spyder/qt/QtWebKit.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/qt/QtWebKit.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,12 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright © 2011 Pierre Raybaut -# Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) - -import os - -if os.environ['QT_API'] == 'pyqt': - from PyQt4.QtWebKit import * # analysis:ignore -else: - from PySide.QtWebKit import * # analysis:ignore \ Kein Zeilenumbruch am Dateiende. diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/requirements.py spyder-3.0.2+dfsg1/spyder/requirements.py --- spyder-2.3.8+dfsg1/spyder/requirements.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/requirements.py 2016-10-25 02:05:23.000000000 +0200 @@ -1,9 +1,8 @@ # -*- coding: utf-8 -*- # -# Copyright © 2009-2013 Pierre Raybaut -# © 2012-2014 anatoly techtonik +# Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) +# (see spyder/__init__.py for details) """Module checking Spyder installation requirements""" @@ -35,16 +34,20 @@ def check_qt(): """Check Qt binding requirements""" - qt_infos = dict(pyqt=("PyQt4", "4.6"), pyside=("PySide", "1.2.0")) + qt_infos = dict(pyqt5=("PyQt5", "5.2"), pyqt=("PyQt4", "4.6")) try: - from spyderlib import qt - package_name, required_ver = qt_infos[qt.API] - actual_ver = qt.__version__ + import qtpy + package_name, required_ver = qt_infos[qtpy.API] + actual_ver = qtpy.PYQT_VERSION if LooseVersion(actual_ver) < LooseVersion(required_ver): show_warning("Please check Spyder installation requirements:\n" "%s %s+ is required (found v%s)." % (package_name, required_ver, actual_ver)) except ImportError: - show_warning("Please check Spyder installation requirements:\n" - "%s %s+ (or %s %s+) is required." - % (qt_infos['pyqt']+qt_infos['pyside'])) + show_warning("Failed to import qtpy.\n" + "Please check Spyder installation requirements:\n\n" + "qtpy 1.1.0+ and either\n" + "%s %s+ or\n" + "%s %s+\n\n" + "are required to run Spyder." + % (qt_infos['pyqt5'] + qt_infos['pyqt'])) diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/rope_patch.py spyder-3.0.2+dfsg1/spyder/rope_patch.py --- spyder-2.3.8+dfsg1/spyder/rope_patch.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/rope_patch.py 2016-10-25 02:05:23.000000000 +0200 @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- # -# Copyright © 2011 Pierre Raybaut +# Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) +# (see spyder/__init__.py for details) """ Patching rope: @@ -20,7 +20,7 @@ http://groups.google.com/group/rope-dev/browse_thread/thread/924c4b5a6268e618 [4] To avoid rope adding a 2 spaces indent to every docstring it gets, because - it breaks the work of Sphinx on the Object Inspector. Also, to better + it breaks the work of Sphinx on the Help plugin. Also, to better control how to get calltips and docstrings of forced builtin objects. [5] To make matplotlib return its docstrings in proper rst, instead of a mix @@ -29,22 +29,23 @@ def apply(): """Monkey patching rope - + See [1], [2], [3], [4] and [5] in module docstring.""" - import rope - if rope.VERSION not in ('0.10.2', '0.9.4-1', '0.9.4', '0.9.3', '0.9.2'): + from spyder.utils.programs import is_module_installed + if is_module_installed('rope', '<0.9.4'): + import rope raise ImportError("rope %s can't be patched" % rope.VERSION) - + # [1] Patching project.Project for compatibility with py2exe/cx_Freeze # distributions - from spyderlib.baseconfig import is_py2exe_or_cx_Freeze + from spyder.config.base import is_py2exe_or_cx_Freeze if is_py2exe_or_cx_Freeze(): from rope.base import project class PatchedProject(project.Project): def _default_config(self): # py2exe/cx_Freeze distribution - from spyderlib.baseconfig import get_module_source_path - fname = get_module_source_path('spyderlib', + from spyder.config.base import get_module_source_path + fname = get_module_source_path('spyder', 'default_config.py') return open(fname, 'rb').read() project.Project = PatchedProject @@ -129,7 +130,7 @@ # 3. get_calltip # To easily get calltips of forced builtins from rope.contrib import codeassist - from spyderlib.utils.dochelpers import getdoc + from spyder.utils.dochelpers import getdoc from rope.base import exceptions class PatchedPyDocExtractor(codeassist.PyDocExtractor): def get_builtin_doc(self, pyobject): @@ -202,9 +203,9 @@ codeassist.PyDocExtractor = PatchedPyDocExtractor - # [5] Get the right matplotlib docstrings for our Object Inspector + # [5] Get the right matplotlib docstrings for Help try: import matplotlib as mpl mpl.rcParams['docstring.hardcopy'] = True - except ImportError: + except: pass diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/scientific_startup.py spyder-3.0.2+dfsg1/spyder/scientific_startup.py --- spyder-2.3.8+dfsg1/spyder/scientific_startup.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/scientific_startup.py 2016-10-25 02:05:23.000000000 +0200 @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- # -# Copyright © 2011 Pierre Raybaut +# Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) +# (see spyder/__init__.py for details) """ Scientific Python startup script @@ -80,7 +80,7 @@ plt_.ion() exec_print("+ guidata %s, guiqwt %s" % (guidata.__version__, guiqwt.__version__)) - except ImportError: + except (ImportError, AssertionError): exec_print() #============================================================================== diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/spyder.py spyder-3.0.2+dfsg1/spyder/spyder.py --- spyder-2.3.8+dfsg1/spyder/spyder.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/spyder.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,2360 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright © 2009-2013 Pierre Raybaut -# Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) - -""" -Spyder, the Scientific PYthon Development EnviRonment -===================================================== - -Developped and maintained by the Spyder Development -Team - -Copyright © 2009 - 2015 Pierre Raybaut -Copyright © 2010 - 2015 The Spyder Development Team -Licensed under the terms of the MIT License -(see spyderlib/__init__.py for details) -""" - -from __future__ import print_function - - -#============================================================================== -# Stdlib imports -#============================================================================== -import atexit -import errno -import os -import os.path as osp -import re -import socket -import shutil -import sys -import threading - - -#============================================================================== -# Keeping a reference to the original sys.exit before patching it -#============================================================================== -ORIGINAL_SYS_EXIT = sys.exit - - -#============================================================================== -# Check requirements -#============================================================================== -from spyderlib import requirements -requirements.check_path() -requirements.check_qt() - - -#============================================================================== -# Windows platforms only: support for hiding the attached console window -#============================================================================== -set_attached_console_visible = None -is_attached_console_visible = None -set_windows_appusermodelid = None -if os.name == 'nt': - from spyderlib.utils.windows import (set_attached_console_visible, - is_attached_console_visible, - set_windows_appusermodelid) - - -#============================================================================== -# Workaround: importing rope.base.project here, otherwise this module can't -# be imported if Spyder was executed from another folder than spyderlib -#============================================================================== -try: - import rope.base.project # analysis:ignore -except ImportError: - pass - - -#============================================================================== -# Don't show IPython ShimWarning's to our users -# TODO: Move to Jupyter imports in 3.1 -#============================================================================== -try: - import warnings - from IPython.utils.shimmodule import ShimWarning - warnings.simplefilter('ignore', ShimWarning) -except: - pass - - -#============================================================================== -# Qt imports -#============================================================================== -from spyderlib.qt.QtGui import (QApplication, QMainWindow, QSplashScreen, - QPixmap, QMessageBox, QMenu, QColor, QShortcut, - QKeySequence, QDockWidget, QAction, - QDesktopServices, QStyleFactory) -from spyderlib.qt.QtCore import (SIGNAL, QPoint, Qt, QSize, QByteArray, QUrl, - QCoreApplication) -from spyderlib.qt.compat import (from_qvariant, getopenfilename, - getsavefilename) -# Avoid a "Cannot mix incompatible Qt library" error on Windows platforms -# when PySide is selected by the QT_API environment variable and when PyQt4 -# is also installed (or any other Qt-based application prepending a directory -# containing incompatible Qt DLLs versions in PATH): -from spyderlib.qt import QtSvg # analysis:ignore - - -#============================================================================== -# Create our QApplication instance here because it's needed to render the -# splash screen created below -#============================================================================== -from spyderlib.utils.qthelpers import qapplication -MAIN_APP = qapplication() - - -#============================================================================== -# Create splash screen out of MainWindow to reduce perceived startup time. -#============================================================================== -from spyderlib.baseconfig import _, get_image_path -SPLASH = QSplashScreen(QPixmap(get_image_path('splash.png'), 'png')) -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() - - -#============================================================================== -# Local utility imports -#============================================================================== -from spyderlib import __version__, __project_url__, __forum_url__, get_versions -from spyderlib.baseconfig import (get_conf_path, get_module_data_path, - get_module_source_path, STDERR, DEBUG, DEV, - debug_print, TEST, SUBFOLDER, MAC_APP_NAME, - running_in_mac_app) -from spyderlib.config import (CONF, EDIT_EXT, IMPORT_EXT, OPEN_FILES_PORT, - is_gtk_desktop) -from spyderlib.cli_options import get_options -from spyderlib import dependencies -from spyderlib.ipythonconfig import IPYTHON_QT_INSTALLED -from spyderlib.userconfig import NoDefault -from spyderlib.utils import encoding, programs -from spyderlib.utils.iofuncs import load_session, save_session, reset_session -from spyderlib.utils.programs import is_module_installed -from spyderlib.utils.introspection import module_completion -from spyderlib.utils.misc import select_port -from spyderlib.py3compat import (PY3, to_text_string, is_text_string, getcwd, - u, qbytearray_to_str, configparser as cp) - - -#============================================================================== -# Local gui imports -#============================================================================== -# NOTE: Move (if possible) import's of widgets and plugins exactly where they -# are needed in MainWindow to speed up perceived startup time (i.e. the time -# from clicking the Spyder icon to showing the splash screen). -try: - from spyderlib.utils.environ import WinUserEnvDialog -except ImportError: - WinUserEnvDialog = None # analysis:ignore - -from spyderlib.utils.qthelpers import (create_action, add_actions, get_icon, - get_std_icon, add_shortcut_to_tooltip, - create_module_bookmark_actions, - create_bookmark_action, - create_program_action, DialogManager, - keybinding, create_python_script_action, - file_uri) -from spyderlib.guiconfig import get_shortcut, remove_deprecated_shortcuts -from spyderlib.otherplugins import get_spyderplugins_mods - - -#============================================================================== -# To save and load temp sessions -#============================================================================== -TEMP_SESSION_PATH = get_conf_path('temp.session.tar') - - -#============================================================================== -# Get the cwd before initializing WorkingDirectory, which sets it to the one -# used in the last session -#============================================================================== -CWD = getcwd() - - -#============================================================================== -# Spyder's main window widgets utilities -#============================================================================== -def get_python_doc_path(): - """ - Return Python documentation path - (Windows: return the PythonXX.chm path if available) - """ - if os.name == 'nt': - doc_path = osp.join(sys.prefix, "Doc") - if not osp.isdir(doc_path): - return - python_chm = [path for path in os.listdir(doc_path) - if re.match(r"(?i)Python[0-9]{3}.chm", path)] - if python_chm: - return file_uri(osp.join(doc_path, python_chm[0])) - else: - vinf = sys.version_info - doc_path = '/usr/share/doc/python%d.%d/html' % (vinf[0], vinf[1]) - python_doc = osp.join(doc_path, "index.html") - if osp.isfile(python_doc): - return file_uri(python_doc) - - -def get_focus_python_shell(): - """Extract and return Python shell from widget - Return None if *widget* is not a Python shell (e.g. IPython kernel)""" - widget = QApplication.focusWidget() - from spyderlib.widgets.shell import PythonShellWidget - from spyderlib.widgets.externalshell.pythonshell import ExternalPythonShell - if isinstance(widget, PythonShellWidget): - return widget - elif isinstance(widget, ExternalPythonShell): - return widget.shell - - -def get_focus_widget_properties(): - """Get properties of focus widget - Returns tuple (widget, properties) where properties is a tuple of - booleans: (is_console, not_readonly, readwrite_editor)""" - widget = QApplication.focusWidget() - from spyderlib.widgets.shell import ShellBaseWidget - from spyderlib.widgets.editor import TextEditBaseWidget - textedit_properties = None - if isinstance(widget, (ShellBaseWidget, TextEditBaseWidget)): - console = isinstance(widget, ShellBaseWidget) - not_readonly = not widget.isReadOnly() - readwrite_editor = not_readonly and not console - textedit_properties = (console, not_readonly, readwrite_editor) - return widget, textedit_properties - - -#============================================================================== -# Main Window -#============================================================================== -class MainWindow(QMainWindow): - """Spyder main window""" - DOCKOPTIONS = QMainWindow.AllowTabbedDocks|QMainWindow.AllowNestedDocks - SPYDER_PATH = get_conf_path('path') - BOOKMARKS = ( - ('numpy', "http://docs.scipy.org/doc/", - _("Numpy and Scipy documentation")), - ('matplotlib', "http://matplotlib.sourceforge.net/contents.html", - _("Matplotlib documentation")), - ('PyQt4', - "http://pyqt.sourceforge.net/Docs/PyQt4/", - _("PyQt4 Reference Guide")), - ('PyQt4', - "http://pyqt.sourceforge.net/Docs/PyQt4/classes.html", - _("PyQt4 API Reference")), - ('xy', "http://code.google.com/p/pythonxy/", - _("Python(x,y)")), - ('winpython', "https://winpython.github.io/", - _("WinPython")) - ) - - def __init__(self, options=None): - QMainWindow.__init__(self) - - qapp = QApplication.instance() - self.default_style = str(qapp.style().objectName()) - - self.dialog_manager = DialogManager() - - self.init_workdir = options.working_directory - self.profile = options.profile - self.multithreaded = options.multithreaded - self.light = options.light - self.new_instance = options.new_instance - - self.debug_print("Start of MainWindow constructor") - - # Use a custom Qt stylesheet - if sys.platform == 'darwin': - spy_path = get_module_source_path('spyderlib') - mac_style = open(osp.join(spy_path, 'mac_stylesheet.qss')).read() - self.setStyleSheet(mac_style) - - # Shortcut management data - self.shortcut_data = [] - - # Loading Spyder path - self.path = [] - self.project_path = [] - if osp.isfile(self.SPYDER_PATH): - self.path, _x = encoding.readlines(self.SPYDER_PATH) - self.path = [name for name in self.path if osp.isdir(name)] - self.remove_path_from_sys_path() - self.add_path_to_sys_path() - self.load_temp_session_action = create_action(self, - _("Reload last session"), - triggered=lambda: - self.load_session(TEMP_SESSION_PATH)) - self.load_session_action = create_action(self, - _("Load session..."), - None, 'fileopen.png', - triggered=self.load_session, - tip=_("Load Spyder session")) - self.save_session_action = create_action(self, - _("Save session and quit..."), - None, 'filesaveas.png', - triggered=self.save_session, - tip=_("Save current session " - "and quit application")) - - # Plugins - self.console = None - self.workingdirectory = None - self.editor = None - self.explorer = None - self.inspector = None - self.onlinehelp = None - self.projectexplorer = None - self.outlineexplorer = None - self.historylog = None - self.extconsole = None - self.ipyconsole = None - self.variableexplorer = None - self.findinfiles = None - self.thirdparty_plugins = [] - - # Preferences - from spyderlib.plugins.configdialog import (MainConfigPage, - ColorSchemeConfigPage) - from spyderlib.plugins.shortcuts import ShortcutsConfigPage - from spyderlib.plugins.runconfig import RunConfigPage - self.general_prefs = [MainConfigPage, ShortcutsConfigPage, - ColorSchemeConfigPage, RunConfigPage] - self.prefs_index = None - self.prefs_dialog_size = None - - # Actions - self.close_dockwidget_action = None - self.find_action = None - self.find_next_action = None - self.find_previous_action = None - self.replace_action = None - self.undo_action = None - self.redo_action = None - self.copy_action = None - self.cut_action = None - self.paste_action = None - self.delete_action = None - self.selectall_action = None - self.maximize_action = None - self.fullscreen_action = None - - # Menu bars - self.file_menu = None - self.file_menu_actions = [] - self.edit_menu = None - self.edit_menu_actions = [] - self.search_menu = None - self.search_menu_actions = [] - self.source_menu = None - self.source_menu_actions = [] - self.run_menu = None - self.run_menu_actions = [] - self.debug_menu = None - self.debug_menu_actions = [] - self.consoles_menu = None - self.consoles_menu_actions = [] - self.tools_menu = None - self.tools_menu_actions = [] - self.external_tools_menu = None # We must keep a reference to this, - # otherwise the external tools menu is lost after leaving setup method - self.external_tools_menu_actions = [] - self.view_menu = None - self.plugins_menu = None - self.toolbars_menu = None - self.help_menu = None - self.help_menu_actions = [] - - # Status bar widgets - self.mem_status = None - self.cpu_status = None - - # Toolbars - self.toolbarslist = [] - self.main_toolbar = None - self.main_toolbar_actions = [] - self.file_toolbar = None - self.file_toolbar_actions = [] - self.edit_toolbar = None - self.edit_toolbar_actions = [] - self.search_toolbar = None - self.search_toolbar_actions = [] - self.source_toolbar = None - self.source_toolbar_actions = [] - self.run_toolbar = None - self.run_toolbar_actions = [] - self.debug_toolbar = None - self.debug_toolbar_actions = [] - - # Set Window title and icon - if DEV is not None: - title = "Spyder %s (Python %s.%s)" % (__version__, - sys.version_info[0], - sys.version_info[1]) - else: - title = "Spyder (Python %s.%s)" % (sys.version_info[0], - sys.version_info[1]) - if DEBUG: - title += " [DEBUG MODE %d]" % DEBUG - self.setWindowTitle(title) - icon_name = 'spyder_light.svg' if self.light else 'spyder.svg' - # Resampling SVG icon only on non-Windows platforms (see Issue 1314): - self.setWindowIcon(get_icon(icon_name, resample=os.name != 'nt')) - if set_windows_appusermodelid != None: - res = set_windows_appusermodelid() - debug_print("appusermodelid: " + str(res)) - - # Showing splash screen - self.splash = SPLASH - if not self.light: - if CONF.get('main', 'current_version', '') != __version__: - CONF.set('main', 'current_version', __version__) - # Execute here the actions to be performed only once after - # each update (there is nothing there for now, but it could - # be useful some day...) - - # List of satellite widgets (registered in add_dockwidget): - self.widgetlist = [] - - # Flags used if closing() is called by the exit() shell command - self.already_closed = False - self.is_starting_up = True - self.is_setting_up = True - - self.floating_dockwidgets = [] - self.window_size = None - self.window_position = None - self.state_before_maximizing = None - self.current_quick_layout = None - self.previous_layout_settings = None - self.last_plugin = None - self.fullscreen_flag = None # isFullscreen does not work as expected - # The following flag remember the maximized state even when - # the window is in fullscreen mode: - self.maximized_flag = None - - # Session manager - self.next_session_name = None - self.save_session_name = None - - # Track which console plugin type had last focus - # True: Console plugin - # False: IPython console plugin - self.last_console_plugin_focus_was_python = True - - # To keep track of the last focused widget - self.last_focused_widget = None - - # Server to open external files on a single instance - self.open_files_server = socket.socket(socket.AF_INET, - socket.SOCK_STREAM, - socket.IPPROTO_TCP) - - self.apply_settings() - self.debug_print("End of MainWindow constructor") - - def debug_print(self, message): - """Debug prints""" - debug_print(message) - - #---- Window setup - def create_toolbar(self, title, object_name, iconsize=24): - """Create and return toolbar with *title* and *object_name*""" - toolbar = self.addToolBar(title) - toolbar.setObjectName(object_name) - toolbar.setIconSize( QSize(iconsize, iconsize) ) - self.toolbarslist.append(toolbar) - return toolbar - - def setup(self): - """Setup main window""" - self.debug_print("*** Start of MainWindow setup ***") - if not self.light: - self.debug_print(" ..core actions") - self.close_dockwidget_action = create_action(self, - _("Close current pane"), - triggered=self.close_current_dockwidget, - context=Qt.ApplicationShortcut) - self.register_shortcut(self.close_dockwidget_action, "_", - "Close pane") - - _text = _("&Find text") - self.find_action = create_action(self, _text, icon='find.png', - tip=_text, triggered=self.find, - context=Qt.WidgetShortcut) - self.register_shortcut(self.find_action, "Editor", "Find text") - self.find_next_action = create_action(self, _("Find &next"), - icon='findnext.png', triggered=self.find_next, - context=Qt.WidgetShortcut) - self.register_shortcut(self.find_next_action, "Editor", - "Find next") - self.find_previous_action = create_action(self, - _("Find &previous"), - icon='findprevious.png', triggered=self.find_previous, - context=Qt.WidgetShortcut) - self.register_shortcut(self.find_previous_action, "Editor", - "Find previous") - _text = _("&Replace text") - self.replace_action = create_action(self, _text, icon='replace.png', - tip=_text, triggered=self.replace, - context=Qt.WidgetShortcut) - self.register_shortcut(self.replace_action, "Editor", - "Replace text") - def create_edit_action(text, tr_text, icon_name): - textseq = text.split(' ') - method_name = textseq[0].lower()+"".join(textseq[1:]) - return create_action(self, tr_text, - shortcut=keybinding(text.replace(' ', '')), - icon=get_icon(icon_name), - triggered=self.global_callback, - data=method_name, - context=Qt.WidgetShortcut) - self.undo_action = create_edit_action("Undo", _("Undo"), - 'undo.png') - self.redo_action = create_edit_action("Redo", _("Redo"), 'redo.png') - self.copy_action = create_edit_action("Copy", _("Copy"), - 'editcopy.png') - self.cut_action = create_edit_action("Cut", _("Cut"), 'editcut.png') - self.paste_action = create_edit_action("Paste", _("Paste"), - 'editpaste.png') - self.delete_action = create_edit_action("Delete", _("Delete"), - 'editdelete.png') - self.selectall_action = create_edit_action("Select All", - _("Select All"), - 'selectall.png') - self.edit_menu_actions = [self.undo_action, self.redo_action, - None, self.cut_action, self.copy_action, - self.paste_action, self.delete_action, - None, self.selectall_action] - self.search_menu_actions = [self.find_action, self.find_next_action, - self.find_previous_action, - self.replace_action] - self.search_toolbar_actions = [self.find_action, - self.find_next_action, - self.replace_action] - - namespace = None - if not self.light: - self.debug_print(" ..toolbars") - # File menu/toolbar - self.file_menu = self.menuBar().addMenu(_("&File")) - self.connect(self.file_menu, SIGNAL("aboutToShow()"), - self.update_file_menu) - self.file_toolbar = self.create_toolbar(_("File toolbar"), - "file_toolbar") - - # Edit menu/toolbar - self.edit_menu = self.menuBar().addMenu(_("&Edit")) - self.edit_toolbar = self.create_toolbar(_("Edit toolbar"), - "edit_toolbar") - - # Search menu/toolbar - self.search_menu = self.menuBar().addMenu(_("&Search")) - self.search_toolbar = self.create_toolbar(_("Search toolbar"), - "search_toolbar") - - # Source menu/toolbar - self.source_menu = self.menuBar().addMenu(_("Sour&ce")) - self.source_toolbar = self.create_toolbar(_("Source toolbar"), - "source_toolbar") - - # Run menu/toolbar - self.run_menu = self.menuBar().addMenu(_("&Run")) - self.run_toolbar = self.create_toolbar(_("Run toolbar"), - "run_toolbar") - - # Debug menu/toolbar - self.debug_menu = self.menuBar().addMenu(_("&Debug")) - self.debug_toolbar = self.create_toolbar(_("Debug toolbar"), - "debug_toolbar") - - # Consoles menu/toolbar - self.consoles_menu = self.menuBar().addMenu(_("C&onsoles")) - - # Tools menu - self.tools_menu = self.menuBar().addMenu(_("&Tools")) - - # View menu - self.view_menu = self.menuBar().addMenu(_("&View")) - - # Help menu - self.help_menu = self.menuBar().addMenu(_("&Help")) - - # Status bar - status = self.statusBar() - status.setObjectName("StatusBar") - status.showMessage(_("Welcome to Spyder!"), 5000) - - - self.debug_print(" ..tools") - # Tools + External Tools - prefs_action = create_action(self, _("Pre&ferences"), - icon='configure.png', - triggered=self.edit_preferences) - self.register_shortcut(prefs_action, "_", "Preferences") - add_shortcut_to_tooltip(prefs_action, context="_", - name="Preferences") - spyder_path_action = create_action(self, - _("PYTHONPATH manager"), - None, 'pythonpath_mgr.png', - triggered=self.path_manager_callback, - tip=_("Python Path Manager"), - menurole=QAction.ApplicationSpecificRole) - update_modules_action = create_action(self, - _("Update module names list"), - triggered=module_completion.reset, - tip=_("Refresh list of module names " - "available in PYTHONPATH")) - self.tools_menu_actions = [prefs_action, spyder_path_action] - if WinUserEnvDialog is not None: - winenv_action = create_action(self, - _("Current user environment variables..."), - icon='win_env.png', - tip=_("Show and edit current user environment " - "variables in Windows registry " - "(i.e. for all sessions)"), - triggered=self.win_env) - self.tools_menu_actions.append(winenv_action) - self.tools_menu_actions += [None, update_modules_action] - - # External Tools submenu - self.external_tools_menu = QMenu(_("External Tools")) - self.external_tools_menu_actions = [] - # Python(x,y) launcher - self.xy_action = create_action(self, - _("Python(x,y) launcher"), - icon=get_icon('pythonxy.png'), - triggered=lambda: - programs.run_python_script('xy', 'xyhome')) - if os.name == 'nt' and is_module_installed('xy'): - self.external_tools_menu_actions.append(self.xy_action) - # WinPython control panel - self.wp_action = create_action(self, _("WinPython control panel"), - icon=get_icon('winpython.svg'), - triggered=lambda: - programs.run_python_script('winpython', 'controlpanel')) - if os.name == 'nt' and is_module_installed('winpython'): - self.external_tools_menu_actions.append(self.wp_action) - # Qt-related tools - additact = [] - for name in ("designer-qt4", "designer"): - qtdact = create_program_action(self, _("Qt Designer"), - name, 'qtdesigner.png') - if qtdact: - break - for name in ("linguist-qt4", "linguist"): - qtlact = create_program_action(self, _("Qt Linguist"), - "linguist", 'qtlinguist.png') - if qtlact: - break - args = ['-no-opengl'] if os.name == 'nt' else [] - qteact = create_python_script_action(self, - _("Qt examples"), 'qt.png', "PyQt4", - osp.join("examples", "demos", - "qtdemo", "qtdemo"), args) - for act in (qtdact, qtlact, qteact): - if act: - additact.append(act) - if additact and (is_module_installed('winpython') or \ - is_module_installed('xy')): - self.external_tools_menu_actions += [None] + additact - - # Guidata and Sift - self.debug_print(" ..sift?") - gdgq_act = [] - if is_module_installed('guidata'): - from guidata import configtools - from guidata import config # (loading icons) analysis:ignore - guidata_icon = configtools.get_icon('guidata.svg') - guidata_act = create_python_script_action(self, - _("guidata examples"), guidata_icon, "guidata", - osp.join("tests", "__init__")) - if guidata_act: - gdgq_act += [guidata_act] - if is_module_installed('guiqwt'): - from guiqwt import config # analysis:ignore - guiqwt_icon = configtools.get_icon('guiqwt.svg') - guiqwt_act = create_python_script_action(self, - _("guiqwt examples"), guiqwt_icon, "guiqwt", - osp.join("tests", "__init__")) - if guiqwt_act: - gdgq_act += [guiqwt_act] - sift_icon = configtools.get_icon('sift.svg') - sift_act = create_python_script_action(self, _("Sift"), - sift_icon, "guiqwt", osp.join("tests", "sift")) - if sift_act: - gdgq_act += [sift_act] - if gdgq_act: - self.external_tools_menu_actions += [None] + gdgq_act - - # ViTables - vitables_act = create_program_action(self, _("ViTables"), - "vitables", 'vitables.png') - if vitables_act: - self.external_tools_menu_actions += [None, vitables_act] - - # Maximize current plugin - self.maximize_action = create_action(self, '', - triggered=self.maximize_dockwidget) - self.register_shortcut(self.maximize_action, "_", - "Maximize pane") - self.__update_maximize_action() - - # Fullscreen mode - self.fullscreen_action = create_action(self, - _("Fullscreen mode"), - triggered=self.toggle_fullscreen) - self.register_shortcut(self.fullscreen_action, "_", - "Fullscreen mode") - add_shortcut_to_tooltip(self.fullscreen_action, context="_", - name="Fullscreen mode") - - # Main toolbar - self.main_toolbar_actions = [self.maximize_action, - self.fullscreen_action, None, - prefs_action, spyder_path_action] - - self.main_toolbar = self.create_toolbar(_("Main toolbar"), - "main_toolbar") - - # Internal console plugin - self.debug_print(" ..plugin: internal console") - from spyderlib.plugins.console import Console - self.console = Console(self, namespace, exitfunc=self.closing, - profile=self.profile, - multithreaded=self.multithreaded, - message=_("Spyder Internal Console\n\n" - "This console is used to report application\n" - "internal errors and to inspect Spyder\n" - "internals with the following commands:\n" - " spy.app, spy.window, dir(spy)\n\n" - "Please don't use it to run your code\n\n")) - self.console.register_plugin() - - # Working directory plugin - self.debug_print(" ..plugin: working directory") - from spyderlib.plugins.workingdirectory import WorkingDirectory - self.workingdirectory = WorkingDirectory(self, self.init_workdir) - self.workingdirectory.register_plugin() - self.toolbarslist.append(self.workingdirectory) - - # Object inspector plugin - if CONF.get('inspector', 'enable'): - self.set_splash(_("Loading object inspector...")) - from spyderlib.plugins.inspector import ObjectInspector - self.inspector = ObjectInspector(self) - self.inspector.register_plugin() - - # Outline explorer widget - if CONF.get('outline_explorer', 'enable'): - self.set_splash(_("Loading outline explorer...")) - from spyderlib.plugins.outlineexplorer import OutlineExplorer - fullpath_sorting = CONF.get('editor', 'fullpath_sorting', True) - self.outlineexplorer = OutlineExplorer(self, - fullpath_sorting=fullpath_sorting) - self.outlineexplorer.register_plugin() - - # Editor plugin - self.set_splash(_("Loading editor...")) - from spyderlib.plugins.editor import Editor - self.editor = Editor(self) - self.editor.register_plugin() - - # Populating file menu entries - quit_action = create_action(self, _("&Quit"), - icon='exit.png', tip=_("Quit"), - triggered=self.console.quit) - self.register_shortcut(quit_action, "_", "Quit") - self.file_menu_actions += [self.load_temp_session_action, - self.load_session_action, - self.save_session_action, - None, quit_action] - self.set_splash("") - - self.debug_print(" ..widgets") - # Find in files - if CONF.get('find_in_files', 'enable'): - from spyderlib.plugins.findinfiles import FindInFiles - self.findinfiles = FindInFiles(self) - self.findinfiles.register_plugin() - - # Explorer - if CONF.get('explorer', 'enable'): - self.set_splash(_("Loading file explorer...")) - from spyderlib.plugins.explorer import Explorer - self.explorer = Explorer(self) - self.explorer.register_plugin() - - # History log widget - if CONF.get('historylog', 'enable'): - self.set_splash(_("Loading history plugin...")) - from spyderlib.plugins.history import HistoryLog - self.historylog = HistoryLog(self) - self.historylog.register_plugin() - - # Online help widget - try: # Qt >= v4.4 - from spyderlib.plugins.onlinehelp import OnlineHelp - except ImportError: # Qt < v4.4 - OnlineHelp = None # analysis:ignore - if CONF.get('onlinehelp', 'enable') and OnlineHelp is not None: - self.set_splash(_("Loading online help...")) - self.onlinehelp = OnlineHelp(self) - self.onlinehelp.register_plugin() - - # Project explorer widget - if CONF.get('project_explorer', 'enable'): - self.set_splash(_("Loading project explorer...")) - from spyderlib.plugins.projectexplorer import ProjectExplorer - self.projectexplorer = ProjectExplorer(self) - self.projectexplorer.register_plugin() - - # External console - if self.light: - # This is necessary to support the --working-directory option: - if self.init_workdir is not None: - os.chdir(self.init_workdir) - else: - self.set_splash(_("Loading external console...")) - from spyderlib.plugins.externalconsole import ExternalConsole - self.extconsole = ExternalConsole(self, light_mode=self.light) - self.extconsole.register_plugin() - - # Namespace browser - if not self.light: - # In light mode, namespace browser is opened inside external console - # Here, it is opened as an independent plugin, in its own dockwidget - self.set_splash(_("Loading namespace browser...")) - from spyderlib.plugins.variableexplorer import VariableExplorer - self.variableexplorer = VariableExplorer(self) - self.variableexplorer.register_plugin() - - # IPython console - if IPYTHON_QT_INSTALLED and not self.light: - self.set_splash(_("Loading IPython console...")) - from spyderlib.plugins.ipythonconsole import IPythonConsole - self.ipyconsole = IPythonConsole(self) - self.ipyconsole.register_plugin() - - if not self.light: - nsb = self.variableexplorer.add_shellwidget(self.console.shell) - self.connect(self.console.shell, SIGNAL('refresh()'), - nsb.refresh_table) - nsb.auto_refresh_button.setEnabled(False) - - self.set_splash(_("Setting up main window...")) - - # Help menu - dep_action = create_action(self, _("Optional dependencies..."), - triggered=self.show_dependencies, - icon='advanced.png') - report_action = create_action(self, - _("Report issue..."), - icon=get_icon('bug.png'), - triggered=self.report_issue) - support_action = create_action(self, - _("Spyder support..."), - triggered=self.google_group) - # Spyder documentation - doc_path = get_module_data_path('spyderlib', relpath="doc", - attr_name='DOCPATH') - # * Trying to find the chm doc - spyder_doc = osp.join(doc_path, "Spyderdoc.chm") - if not osp.isfile(spyder_doc): - spyder_doc = osp.join(doc_path, os.pardir, "Spyderdoc.chm") - # * Trying to find the html doc - if not osp.isfile(spyder_doc): - spyder_doc = osp.join(doc_path, "index.html") - # * Trying to find the development-version html doc - if not osp.isfile(spyder_doc): - spyder_doc = osp.join(get_module_source_path('spyderlib'), - os.pardir, 'build', 'lib', 'spyderlib', - 'doc', "index.html") - # * If we totally fail, point to our web build - if not osp.isfile(spyder_doc): - spyder_doc = 'http://pythonhosted.org/spyder' - else: - spyder_doc = file_uri(spyder_doc) - doc_action = create_bookmark_action(self, spyder_doc, - _("Spyder documentation"), shortcut="F1", - icon=get_std_icon('DialogHelpButton')) - tut_action = create_action(self, _("Spyder tutorial"), - triggered=self.inspector.show_tutorial) - self.help_menu_actions = [doc_action, tut_action, None, - report_action, dep_action, support_action, - None] - # Python documentation - if get_python_doc_path() is not None: - pydoc_act = create_action(self, _("Python documentation"), - triggered=lambda: - programs.start_file(get_python_doc_path())) - self.help_menu_actions.append(pydoc_act) - # IPython documentation - if self.ipyconsole is not None: - ipython_menu = QMenu(_("IPython documentation"), self) - intro_action = create_action(self, _("Intro to IPython"), - triggered=self.ipyconsole.show_intro) - quickref_action = create_action(self, _("Quick reference"), - triggered=self.ipyconsole.show_quickref) - guiref_action = create_action(self, _("Console help"), - triggered=self.ipyconsole.show_guiref) - add_actions(ipython_menu, (intro_action, guiref_action, - quickref_action)) - self.help_menu_actions.append(ipython_menu) - # Windows-only: documentation located in sys.prefix/Doc - ipm_actions = [] - def add_ipm_action(text, path): - """Add installed Python module doc action to help submenu""" - path = file_uri(path) - action = create_action(self, text, - icon='%s.png' % osp.splitext(path)[1][1:], - triggered=lambda path=path: programs.start_file(path)) - ipm_actions.append(action) - sysdocpth = osp.join(sys.prefix, 'Doc') - if osp.isdir(sysdocpth): # exists on Windows, except frozen dist. - for docfn in os.listdir(sysdocpth): - pt = r'([a-zA-Z\_]*)(doc)?(-dev)?(-ref)?(-user)?.(chm|pdf)' - match = re.match(pt, docfn) - if match is not None: - pname = match.groups()[0] - if pname not in ('Python', ): - add_ipm_action(pname, osp.join(sysdocpth, docfn)) - # Documentation provided by Python(x,y), if available - try: - from xy.config import DOC_PATH as xy_doc_path - xydoc = osp.join(xy_doc_path, "Libraries") - def add_xydoc(text, pathlist): - for path in pathlist: - if osp.exists(path): - add_ipm_action(text, path) - break - add_xydoc(_("Python(x,y) documentation folder"), - [xy_doc_path]) - add_xydoc(_("IPython documentation"), - [osp.join(xydoc, "IPython", "ipythondoc.chm")]) - add_xydoc(_("guidata documentation"), - [osp.join(xydoc, "guidata", "guidatadoc.chm"), - r"D:\Python\guidata\build\doc_chm\guidatadoc.chm"]) - add_xydoc(_("guiqwt documentation"), - [osp.join(xydoc, "guiqwt", "guiqwtdoc.chm"), - r"D:\Python\guiqwt\build\doc_chm\guiqwtdoc.chm"]) - add_xydoc(_("Matplotlib documentation"), - [osp.join(xydoc, "matplotlib", "Matplotlibdoc.chm"), - osp.join(xydoc, "matplotlib", "Matplotlib.pdf")]) - add_xydoc(_("NumPy documentation"), - [osp.join(xydoc, "NumPy", "numpy.chm")]) - add_xydoc(_("NumPy reference guide"), - [osp.join(xydoc, "NumPy", "numpy-ref.pdf")]) - add_xydoc(_("NumPy user guide"), - [osp.join(xydoc, "NumPy", "numpy-user.pdf")]) - add_xydoc(_("SciPy documentation"), - [osp.join(xydoc, "SciPy", "scipy.chm"), - osp.join(xydoc, "SciPy", "scipy-ref.pdf")]) - except (ImportError, KeyError, RuntimeError): - pass - # Installed Python modules submenu (Windows only) - if ipm_actions: - pymods_menu = QMenu(_("Installed Python modules"), self) - add_actions(pymods_menu, ipm_actions) - self.help_menu_actions.append(pymods_menu) - # Online documentation - web_resources = QMenu(_("Online documentation")) - webres_actions = create_module_bookmark_actions(self, - self.BOOKMARKS) - webres_actions.insert(2, None) - webres_actions.insert(5, None) - add_actions(web_resources, webres_actions) - self.help_menu_actions.append(web_resources) - # Qt assistant link - qta_exe = "assistant-qt4" if sys.platform.startswith('linux') else \ - "assistant" - qta_act = create_program_action(self, _("Qt documentation"), - qta_exe) - if qta_act: - self.help_menu_actions += [qta_act, None] - # About Spyder - about_action = create_action(self, - _("About %s...") % "Spyder", - icon=get_std_icon('MessageBoxInformation'), - triggered=self.about) - self.help_menu_actions += [None, about_action] - - # Status bar widgets - from spyderlib.widgets.status import MemoryStatus, CPUStatus - self.mem_status = MemoryStatus(self, status) - self.cpu_status = CPUStatus(self, status) - self.apply_statusbar_settings() - - # Third-party plugins - for mod in get_spyderplugins_mods(prefix='p_', extension='.py'): - try: - plugin = mod.PLUGIN_CLASS(self) - self.thirdparty_plugins.append(plugin) - plugin.register_plugin() - except AttributeError as error: - print("%s: %s" % (mod, str(error)), file=STDERR) - - # View menu - self.plugins_menu = QMenu(_("Panes"), self) - self.toolbars_menu = QMenu(_("Toolbars"), self) - self.view_menu.addMenu(self.plugins_menu) - self.view_menu.addMenu(self.toolbars_menu) - reset_layout_action = create_action(self, _("Reset window layout"), - triggered=self.reset_window_layout) - quick_layout_menu = QMenu(_("Custom window layouts"), self) - ql_actions = [] - for index in range(1, 4): - if index > 0: - ql_actions += [None] - qli_act = create_action(self, - _("Switch to/from layout %d") % index, - triggered=lambda i=index: - self.quick_layout_switch(i)) - self.register_shortcut(qli_act, "_", - "Switch to/from layout %d" % index) - qlsi_act = create_action(self, _("Set layout %d") % index, - triggered=lambda i=index: - self.quick_layout_set(i)) - self.register_shortcut(qlsi_act, "_", "Set layout %d" % index) - ql_actions += [qli_act, qlsi_act] - add_actions(quick_layout_menu, ql_actions) - if set_attached_console_visible is not None: - cmd_act = create_action(self, - _("Attached console window (debugging)"), - toggled=set_attached_console_visible) - cmd_act.setChecked(is_attached_console_visible()) - add_actions(self.view_menu, (None, cmd_act)) - add_actions(self.view_menu, (None, self.fullscreen_action, - self.maximize_action, - self.close_dockwidget_action, None, - reset_layout_action, - quick_layout_menu)) - - # Adding external tools action to "Tools" menu - if self.external_tools_menu_actions: - external_tools_act = create_action(self, _("External Tools")) - external_tools_act.setMenu(self.external_tools_menu) - self.tools_menu_actions += [None, external_tools_act] - - # Filling out menu/toolbar entries: - add_actions(self.file_menu, self.file_menu_actions) - add_actions(self.edit_menu, self.edit_menu_actions) - add_actions(self.search_menu, self.search_menu_actions) - add_actions(self.source_menu, self.source_menu_actions) - add_actions(self.run_menu, self.run_menu_actions) - add_actions(self.debug_menu, self.debug_menu_actions) - add_actions(self.consoles_menu, self.consoles_menu_actions) - add_actions(self.tools_menu, self.tools_menu_actions) - add_actions(self.external_tools_menu, - self.external_tools_menu_actions) - add_actions(self.help_menu, self.help_menu_actions) - - add_actions(self.main_toolbar, self.main_toolbar_actions) - add_actions(self.file_toolbar, self.file_toolbar_actions) - add_actions(self.edit_toolbar, self.edit_toolbar_actions) - add_actions(self.search_toolbar, self.search_toolbar_actions) - add_actions(self.source_toolbar, self.source_toolbar_actions) - add_actions(self.debug_toolbar, self.debug_toolbar_actions) - add_actions(self.run_toolbar, self.run_toolbar_actions) - - # Apply all defined shortcuts (plugins + 3rd-party plugins) - self.apply_shortcuts() - #self.remove_deprecated_shortcuts() - - # Emitting the signal notifying plugins that main window menu and - # toolbar actions are all defined: - self.emit(SIGNAL('all_actions_defined()')) - - # Window set-up - self.debug_print("Setting up window...") - self.setup_layout(default=False) - - self.splash.hide() - - # Enabling tear off for all menus except help menu - if CONF.get('main', 'tear_off_menus'): - for child in self.menuBar().children(): - if isinstance(child, QMenu) and child != self.help_menu: - child.setTearOffEnabled(True) - - # Menu about to show - for child in self.menuBar().children(): - if isinstance(child, QMenu): - self.connect(child, SIGNAL("aboutToShow()"), - self.update_edit_menu) - - self.debug_print("*** End of MainWindow setup ***") - self.is_starting_up = False - - def post_visible_setup(self): - """Actions to be performed only after the main window's `show` method - was triggered""" - self.emit(SIGNAL('restore_scrollbar_position()')) - - if self.projectexplorer is not None: - self.projectexplorer.check_for_io_errors() - - # Remove our temporary dir - atexit.register(self.remove_tmpdir) - - # Remove settings test directory - if TEST is not None: - import tempfile - conf_dir = osp.join(tempfile.gettempdir(), SUBFOLDER) - atexit.register(shutil.rmtree, conf_dir, ignore_errors=True) - - # [Workaround for Issue 880] - # QDockWidget objects are not painted if restored as floating - # windows, so we must dock them before showing the mainwindow, - # then set them again as floating windows here. - for widget in self.floating_dockwidgets: - widget.setFloating(True) - - # In MacOS X 10.7 our app is not displayed after initialized (I don't - # know why because this doesn't happen when started from the terminal), - # so we need to resort to this hack to make it appear. - if running_in_mac_app(): - import subprocess - idx = __file__.index(MAC_APP_NAME) - app_path = __file__[:idx] - subprocess.call(['open', app_path + MAC_APP_NAME]) - - # Server to maintain just one Spyder instance and open files in it if - # the user tries to start other instances with - # $ spyder foo.py - if CONF.get('main', 'single_instance') and not self.new_instance: - t = threading.Thread(target=self.start_open_files_server) - t.setDaemon(True) - t.start() - - # Connect the window to the signal emmited by the previous server - # when it gets a client connected to it - self.connect(self, SIGNAL('open_external_file(QString)'), - lambda fname: self.open_external_file(fname)) - - # Create Plugins and toolbars submenus - if not self.light: - self.create_plugins_menu() - self.create_toolbars_menu() - - # Open a Python console for light mode - if self.light: - self.extconsole.open_interpreter() - self.extconsole.setMinimumHeight(0) - - if not self.light: - # Hide Internal Console so that people don't use it instead of - # the External or IPython ones - if self.console.dockwidget.isVisible() and DEV is None: - self.console.toggle_view_action.setChecked(False) - self.console.dockwidget.hide() - - # Show the Object Inspector and Consoles by default - plugins_to_show = [self.inspector] - if self.ipyconsole is not None: - if self.ipyconsole.isvisible: - plugins_to_show += [self.extconsole, self.ipyconsole] - else: - plugins_to_show += [self.ipyconsole, self.extconsole] - else: - plugins_to_show += [self.extconsole] - for plugin in plugins_to_show: - if plugin.dockwidget.isVisible(): - plugin.dockwidget.raise_() - - # Show history file if no console is visible - ipy_visible = self.ipyconsole is not None and self.ipyconsole.isvisible - if not self.extconsole.isvisible and not ipy_visible: - self.historylog.add_history(get_conf_path('history.py')) - - # Give focus to the Editor - if self.editor.dockwidget.isVisible(): - try: - self.editor.get_focus_widget().setFocus() - except AttributeError: - pass - - self.is_setting_up = False - - def load_window_settings(self, prefix, default=False, section='main'): - """Load window layout settings from userconfig-based configuration - with *prefix*, under *section* - default: if True, do not restore inner layout""" - get_func = CONF.get_default if default else CONF.get - window_size = get_func(section, prefix+'size') - prefs_dialog_size = get_func(section, prefix+'prefs_dialog_size') - if default: - hexstate = None - else: - hexstate = get_func(section, prefix+'state', None) - pos = get_func(section, prefix+'position') - is_maximized = get_func(section, prefix+'is_maximized') - is_fullscreen = get_func(section, prefix+'is_fullscreen') - return hexstate, window_size, prefs_dialog_size, pos, is_maximized, \ - is_fullscreen - - def get_window_settings(self): - """Return current window settings - Symetric to the 'set_window_settings' setter""" - window_size = (self.window_size.width(), self.window_size.height()) - is_fullscreen = self.isFullScreen() - if is_fullscreen: - is_maximized = self.maximized_flag - else: - is_maximized = self.isMaximized() - pos = (self.window_position.x(), self.window_position.y()) - prefs_dialog_size = (self.prefs_dialog_size.width(), - self.prefs_dialog_size.height()) - hexstate = qbytearray_to_str(self.saveState()) - return (hexstate, window_size, prefs_dialog_size, pos, is_maximized, - is_fullscreen) - - def set_window_settings(self, hexstate, window_size, prefs_dialog_size, - pos, is_maximized, is_fullscreen): - """Set window settings - Symetric to the 'get_window_settings' accessor""" - self.setUpdatesEnabled(False) - self.window_size = QSize(window_size[0], window_size[1]) # width,height - self.prefs_dialog_size = QSize(prefs_dialog_size[0], - prefs_dialog_size[1]) # width,height - self.window_position = QPoint(pos[0], pos[1]) # x,y - self.setWindowState(Qt.WindowNoState) - self.resize(self.window_size) - self.move(self.window_position) - if not self.light: - # Window layout - if hexstate: - self.restoreState( QByteArray().fromHex(str(hexstate)) ) - # [Workaround for Issue 880] - # QDockWidget objects are not painted if restored as floating - # windows, so we must dock them before showing the mainwindow. - for widget in self.children(): - if isinstance(widget, QDockWidget) and widget.isFloating(): - self.floating_dockwidgets.append(widget) - widget.setFloating(False) - # Is fullscreen? - if is_fullscreen: - self.setWindowState(Qt.WindowFullScreen) - self.__update_fullscreen_action() - # Is maximized? - if is_fullscreen: - self.maximized_flag = is_maximized - elif is_maximized: - self.setWindowState(Qt.WindowMaximized) - self.setUpdatesEnabled(True) - - def save_current_window_settings(self, prefix, section='main'): - """Save current window settings with *prefix* in - the userconfig-based configuration, under *section*""" - win_size = self.window_size - prefs_size = self.prefs_dialog_size - - CONF.set(section, prefix+'size', (win_size.width(), win_size.height())) - CONF.set(section, prefix+'prefs_dialog_size', - (prefs_size.width(), prefs_size.height())) - CONF.set(section, prefix+'is_maximized', self.isMaximized()) - CONF.set(section, prefix+'is_fullscreen', self.isFullScreen()) - pos = self.window_position - CONF.set(section, prefix+'position', (pos.x(), pos.y())) - if not self.light: - self.maximize_dockwidget(restore=True)# Restore non-maximized layout - qba = self.saveState() - CONF.set(section, prefix+'state', qbytearray_to_str(qba)) - CONF.set(section, prefix+'statusbar', - not self.statusBar().isHidden()) - - def tabify_plugins(self, first, second): - """Tabify plugin dockwigdets""" - self.tabifyDockWidget(first.dockwidget, second.dockwidget) - - def setup_layout(self, default=False): - """Setup window layout""" - prefix = ('lightwindow' if self.light else 'window') + '/' - (hexstate, window_size, prefs_dialog_size, pos, is_maximized, - is_fullscreen) = self.load_window_settings(prefix, default) - - if hexstate is None and not self.light: - # First Spyder execution: - # trying to set-up the dockwidget/toolbar positions to the best - # appearance possible - splitting = ( - (self.projectexplorer, self.editor, Qt.Horizontal), - (self.editor, self.outlineexplorer, Qt.Horizontal), - (self.outlineexplorer, self.inspector, Qt.Horizontal), - (self.inspector, self.console, Qt.Vertical), - ) - for first, second, orientation in splitting: - if first is not None and second is not None: - self.splitDockWidget(first.dockwidget, second.dockwidget, - orientation) - for first, second in ((self.console, self.extconsole), - (self.extconsole, self.ipyconsole), - (self.ipyconsole, self.historylog), - (self.inspector, self.variableexplorer), - (self.variableexplorer, self.onlinehelp), - (self.onlinehelp, self.explorer), - (self.explorer, self.findinfiles), - ): - if first is not None and second is not None: - self.tabify_plugins(first, second) - for plugin in [self.findinfiles, self.onlinehelp, self.console, - ]+self.thirdparty_plugins: - if plugin is not None: - plugin.dockwidget.close() - for plugin in (self.inspector, self.extconsole): - if plugin is not None: - plugin.dockwidget.raise_() - self.extconsole.setMinimumHeight(250) - hidden_toolbars = [self.source_toolbar, self.edit_toolbar, - self.search_toolbar] - for toolbar in hidden_toolbars: - toolbar.close() - for plugin in (self.projectexplorer, self.outlineexplorer): - plugin.dockwidget.close() - - self.set_window_settings(hexstate, window_size, prefs_dialog_size, pos, - is_maximized, is_fullscreen) - - for plugin in self.widgetlist: - plugin.initialize_plugin_in_mainwindow_layout() - - def reset_window_layout(self): - """Reset window layout to default""" - answer = QMessageBox.warning(self, _("Warning"), - _("Window layout will be reset to default settings: " - "this affects window position, size and dockwidgets.\n" - "Do you want to continue?"), - QMessageBox.Yes | QMessageBox.No) - if answer == QMessageBox.Yes: - self.setup_layout(default=True) - - def quick_layout_switch(self, index): - """Switch to quick layout number *index*""" - if self.current_quick_layout == index: - self.set_window_settings(*self.previous_layout_settings) - self.current_quick_layout = None - else: - try: - settings = self.load_window_settings('layout_%d/' % index, - section='quick_layouts') - except cp.NoOptionError: - QMessageBox.critical(self, _("Warning"), - _("Quick switch layout #%d has not yet " - "been defined.") % index) - return - self.previous_layout_settings = self.get_window_settings() - self.set_window_settings(*settings) - self.current_quick_layout = index - - def quick_layout_set(self, index): - """Save current window settings as quick layout number *index*""" - self.save_current_window_settings('layout_%d/' % index, - section='quick_layouts') - - def plugin_focus_changed(self): - """Focus has changed from one plugin to another""" - if self.light: - # There is currently no point doing the following in light mode - return - self.update_edit_menu() - self.update_search_menu() - - # Now deal with Python shell and IPython plugins - shell = get_focus_python_shell() - if shell is not None: - # A Python shell widget has focus - self.last_console_plugin_focus_was_python = True - if self.inspector is not None: - # The object inspector may be disabled in .spyder.ini - self.inspector.set_shell(shell) - from spyderlib.widgets.externalshell import pythonshell - if isinstance(shell, pythonshell.ExtPythonShellWidget): - shell = shell.parent() - self.variableexplorer.set_shellwidget_from_id(id(shell)) - elif self.ipyconsole is not None: - focus_client = self.ipyconsole.get_focus_client() - if focus_client is not None: - self.last_console_plugin_focus_was_python = False - kwid = focus_client.kernel_widget_id - if kwid is not None: - idx = self.extconsole.get_shell_index_from_id(kwid) - if idx is not None: - kw = self.extconsole.shellwidgets[idx] - if self.inspector is not None: - self.inspector.set_shell(kw) - self.variableexplorer.set_shellwidget_from_id(kwid) - # Setting the kernel widget as current widget for the - # external console's tabwidget: this is necessary for - # the editor/console link to be working (otherwise, - # features like "Execute in current interpreter" will - # not work with IPython clients unless the associated - # IPython kernel has been selected in the external - # console... that's not brilliant, but it works for - # now: we shall take action on this later - self.extconsole.tabwidget.setCurrentWidget(kw) - focus_client.get_control().setFocus() - - def update_file_menu(self): - """Update file menu""" - self.load_temp_session_action.setEnabled(osp.isfile(TEMP_SESSION_PATH)) - - def update_edit_menu(self): - """Update edit menu""" - if self.menuBar().hasFocus(): - return - # Disabling all actions to begin with - for child in self.edit_menu.actions(): - child.setEnabled(False) - - widget, textedit_properties = get_focus_widget_properties() - if textedit_properties is None: # widget is not an editor/console - return - #!!! Below this line, widget is expected to be a QPlainTextEdit instance - console, not_readonly, readwrite_editor = textedit_properties - - # Editor has focus and there is no file opened in it - if not console and not_readonly and not self.editor.is_file_opened(): - return - - self.selectall_action.setEnabled(True) - - # Undo, redo - self.undo_action.setEnabled( readwrite_editor \ - and widget.document().isUndoAvailable() ) - self.redo_action.setEnabled( readwrite_editor \ - and widget.document().isRedoAvailable() ) - - # Copy, cut, paste, delete - has_selection = widget.has_selected_text() - self.copy_action.setEnabled(has_selection) - self.cut_action.setEnabled(has_selection and not_readonly) - self.paste_action.setEnabled(not_readonly) - self.delete_action.setEnabled(has_selection and not_readonly) - - # Comment, uncomment, indent, unindent... - if not console and not_readonly: - # This is the editor and current file is writable - for action in self.editor.edit_menu_actions: - action.setEnabled(True) - - def update_search_menu(self): - """Update search menu""" - if self.menuBar().hasFocus(): - return - # Disabling all actions to begin with - for child in [self.find_action, self.find_next_action, - self.find_previous_action, self.replace_action]: - child.setEnabled(False) - - widget, textedit_properties = get_focus_widget_properties() - for action in self.editor.search_menu_actions: - action.setEnabled(self.editor.isAncestorOf(widget)) - if textedit_properties is None: # widget is not an editor/console - return - #!!! Below this line, widget is expected to be a QPlainTextEdit instance - _x, _y, readwrite_editor = textedit_properties - for action in [self.find_action, self.find_next_action, - self.find_previous_action]: - action.setEnabled(True) - self.replace_action.setEnabled(readwrite_editor) - self.replace_action.setEnabled(readwrite_editor) - - def create_plugins_menu(self): - order = ['editor', 'console', 'ipython_console', 'variable_explorer', - 'inspector', None, 'explorer', 'outline_explorer', - 'project_explorer', 'find_in_files', None, 'historylog', - 'profiler', 'breakpoints', 'pylint', None, - 'onlinehelp', 'internal_console'] - for plugin in self.widgetlist: - action = plugin.toggle_view_action - action.setChecked(plugin.dockwidget.isVisible()) - try: - name = plugin.CONF_SECTION - pos = order.index(name) - except ValueError: - pos = None - if pos is not None: - order[pos] = action - else: - order.append(action) - actions = order[:] - for action in order: - if type(action) is str: - actions.remove(action) - add_actions(self.plugins_menu, actions) - - def create_toolbars_menu(self): - order = ['file_toolbar', 'run_toolbar', 'debug_toolbar', - 'main_toolbar', 'Global working directory', None, - 'search_toolbar', 'edit_toolbar', 'source_toolbar'] - for toolbar in self.toolbarslist: - action = toolbar.toggleViewAction() - name = toolbar.objectName() - try: - pos = order.index(name) - except ValueError: - pos = None - if pos is not None: - order[pos] = action - else: - order.append(action) - add_actions(self.toolbars_menu, order) - - def createPopupMenu(self): - if self.light: - menu = self.createPopupMenu() - else: - menu = QMenu('', self) - actions = self.help_menu_actions[:3] + \ - [None, self.help_menu_actions[-1]] - add_actions(menu, actions) - return menu - - def set_splash(self, message): - """Set splash message""" - if message: - self.debug_print(message) - self.splash.show() - self.splash.showMessage(message, Qt.AlignBottom | Qt.AlignCenter | - Qt.AlignAbsolute, QColor(Qt.white)) - QApplication.processEvents() - - def remove_tmpdir(self): - """Remove Spyder temporary directory""" - shutil.rmtree(programs.TEMPDIR, ignore_errors=True) - - def closeEvent(self, event): - """closeEvent reimplementation""" - if self.closing(True): - event.accept() - else: - event.ignore() - - def resizeEvent(self, event): - """Reimplement Qt method""" - if not self.isMaximized() and not self.fullscreen_flag: - self.window_size = self.size() - QMainWindow.resizeEvent(self, event) - - def moveEvent(self, event): - """Reimplement Qt method""" - if not self.isMaximized() and not self.fullscreen_flag: - self.window_position = self.pos() - QMainWindow.moveEvent(self, event) - - def hideEvent(self, event): - """Reimplement Qt method""" - if not self.light: - for plugin in self.widgetlist: - if plugin.isAncestorOf(self.last_focused_widget): - plugin.visibility_changed(True) - QMainWindow.hideEvent(self, event) - - def change_last_focused_widget(self, old, now): - """To keep track of to the last focused widget""" - if (now is None and QApplication.activeWindow() is not None): - QApplication.activeWindow().setFocus() - self.last_focused_widget = QApplication.focusWidget() - elif now is not None: - self.last_focused_widget = now - - def closing(self, cancelable=False): - """Exit tasks""" - if self.already_closed or self.is_starting_up: - return True - prefix = ('lightwindow' if self.light else 'window') + '/' - self.save_current_window_settings(prefix) - if CONF.get('main', 'single_instance'): - self.open_files_server.close() - for widget in self.widgetlist: - if not widget.closing_plugin(cancelable): - return False - self.dialog_manager.close_all() - self.already_closed = True - return True - - def add_dockwidget(self, child): - """Add QDockWidget and toggleViewAction""" - dockwidget, location = child.create_dockwidget() - if CONF.get('main', 'vertical_dockwidget_titlebars'): - dockwidget.setFeatures(dockwidget.features()| - QDockWidget.DockWidgetVerticalTitleBar) - self.addDockWidget(location, dockwidget) - self.widgetlist.append(child) - - def close_current_dockwidget(self): - widget = QApplication.focusWidget() - for plugin in self.widgetlist: - if plugin.isAncestorOf(widget): - plugin.dockwidget.hide() - break - - def __update_maximize_action(self): - if self.state_before_maximizing is None: - text = _("Maximize current pane") - tip = _("Maximize current pane") - icon = "maximize.png" - else: - text = _("Restore current pane") - tip = _("Restore pane to its original size") - icon = "unmaximize.png" - self.maximize_action.setText(text) - self.maximize_action.setIcon(get_icon(icon)) - self.maximize_action.setToolTip(tip) - - def maximize_dockwidget(self, restore=False): - """Shortcut: Ctrl+Alt+Shift+M - First call: maximize current dockwidget - Second call (or restore=True): restore original window layout""" - if self.state_before_maximizing is None: - if restore: - return - # No plugin is currently maximized: maximizing focus plugin - self.state_before_maximizing = self.saveState() - focus_widget = QApplication.focusWidget() - for plugin in self.widgetlist: - plugin.dockwidget.hide() - if plugin.isAncestorOf(focus_widget): - self.last_plugin = plugin - self.last_plugin.dockwidget.toggleViewAction().setDisabled(True) - self.setCentralWidget(self.last_plugin) - self.last_plugin.ismaximized = True - # Workaround to solve an issue with editor's outline explorer: - # (otherwise the whole plugin is hidden and so is the outline explorer - # and the latter won't be refreshed if not visible) - self.last_plugin.show() - self.last_plugin.visibility_changed(True) - if self.last_plugin is self.editor: - # Automatically show the outline if the editor was maximized: - self.addDockWidget(Qt.RightDockWidgetArea, - self.outlineexplorer.dockwidget) - self.outlineexplorer.dockwidget.show() - else: - # Restore original layout (before maximizing current dockwidget) - self.last_plugin.dockwidget.setWidget(self.last_plugin) - self.last_plugin.dockwidget.toggleViewAction().setEnabled(True) - self.setCentralWidget(None) - self.last_plugin.ismaximized = False - self.restoreState(self.state_before_maximizing) - self.state_before_maximizing = None - self.last_plugin.get_focus_widget().setFocus() - self.__update_maximize_action() - - def __update_fullscreen_action(self): - if self.isFullScreen(): - icon = "window_nofullscreen.png" - else: - icon = "window_fullscreen.png" - self.fullscreen_action.setIcon(get_icon(icon)) - - def toggle_fullscreen(self): - if self.isFullScreen(): - self.fullscreen_flag = False - self.showNormal() - if self.maximized_flag: - self.showMaximized() - else: - self.maximized_flag = self.isMaximized() - self.fullscreen_flag = True - self.showFullScreen() - self.__update_fullscreen_action() - - def add_to_toolbar(self, toolbar, widget): - """Add widget actions to toolbar""" - actions = widget.toolbar_actions - if actions is not None: - add_actions(toolbar, actions) - - def about(self): - """About Spyder""" - versions = get_versions() - # Show Mercurial revision for development version - revlink = '' - if versions['revision']: - rev = versions['revision'] - revlink = " (Commit: %s)" % (rev, rev) - QMessageBox.about(self, - _("About %s") % "Spyder", - """Spyder %s %s -
    The Scientific PYthon Development EnviRonment -

    Copyright © 2009 - 2015 Pierre Raybaut -
    Copyright © 2010 - 2015 The Spyder Development Team -
    Licensed under the terms of the MIT License -

    Created by Pierre Raybaut -
    Developed and maintained by the - Spyder Development Team -
    Many thanks to all the Spyder beta-testers and regular users. -

    Most of the icons come from the Crystal Project - (© 2006-2007 Everaldo Coelho). Other icons by - Yusuke Kamiyamane - (all rights reserved) and by - - The Oxygen icon theme. -

    For bug reports and feature requests, please go - to our Github website. For discussions around the - project, please go to our Google Group -

    This project is part of a larger effort to promote and - facilitate the use of Python for scientific and engineering - software development. The popular Python distributions - Anaconda, - WinPython and - Python(x,y) - also contribute to this plan. -

    Python %s %dbits, Qt %s, %s %s on %s""" - % (versions['spyder'], revlink, __project_url__, - __project_url__, __forum_url__, versions['python'], - versions['bitness'], versions['qt'], versions['qt_api'], - versions['qt_api_ver'], versions['system'])) - - def show_dependencies(self): - """Show Spyder's Optional Dependencies dialog box""" - from spyderlib.widgets.dependencies import DependenciesDialog - dlg = DependenciesDialog(None) - dlg.set_data(dependencies.DEPENDENCIES) - dlg.show() - dlg.exec_() - - def report_issue(self): - if PY3: - from urllib.parse import quote - else: - from urllib import quote # analysis:ignore - versions = get_versions() - # Get git revision for development version - revision = '' - if versions['revision']: - revision = versions['revision'] - issue_template = """\ -## Description - -**What steps will reproduce the problem?** - -1. -2. -3. - -**What is the expected output? What do you see instead?** - - -**Please provide any additional information below** - - -## Version and main components - -* Spyder Version: %s %s -* Python Version: %s -* Qt Versions: %s, %s %s on %s - -## Optional dependencies -``` -%s -``` -""" % (versions['spyder'], - revision, - versions['python'], - versions['qt'], - versions['qt_api'], - versions['qt_api_ver'], - versions['system'], - dependencies.status()) - - url = QUrl("https://github.com/spyder-ide/spyder/issues/new") - url.addEncodedQueryItem("body", quote(issue_template)) - QDesktopServices.openUrl(url) - - def google_group(self): - url = QUrl("http://groups.google.com/group/spyderlib") - QDesktopServices.openUrl(url) - - #---- Global callbacks (called from plugins) - def get_current_editor_plugin(self): - """Return editor plugin which has focus: - console, extconsole, editor, inspector or historylog""" - if self.light: - return self.extconsole - widget = QApplication.focusWidget() - from spyderlib.widgets.editor import TextEditBaseWidget - from spyderlib.widgets.shell import ShellBaseWidget - if not isinstance(widget, (TextEditBaseWidget, ShellBaseWidget)): - return - for plugin in self.widgetlist: - if plugin.isAncestorOf(widget): - return plugin - else: - # External Editor window - plugin = widget - from spyderlib.widgets.editor import EditorWidget - while not isinstance(plugin, EditorWidget): - plugin = plugin.parent() - return plugin - - def find(self): - """Global find callback""" - plugin = self.get_current_editor_plugin() - if plugin is not None: - plugin.find_widget.show() - plugin.find_widget.search_text.setFocus() - return plugin - - def find_next(self): - """Global find next callback""" - plugin = self.get_current_editor_plugin() - if plugin is not None: - plugin.find_widget.find_next() - - def find_previous(self): - """Global find previous callback""" - plugin = self.get_current_editor_plugin() - if plugin is not None: - plugin.find_widget.find_previous() - - def replace(self): - """Global replace callback""" - plugin = self.find() - if plugin is not None: - plugin.find_widget.show_replace() - - def global_callback(self): - """Global callback""" - widget = QApplication.focusWidget() - action = self.sender() - callback = from_qvariant(action.data(), to_text_string) - from spyderlib.widgets.editor import TextEditBaseWidget - if isinstance(widget, TextEditBaseWidget): - getattr(widget, callback)() - - def redirect_internalshell_stdio(self, state): - if state: - self.console.shell.interpreter.redirect_stds() - else: - self.console.shell.interpreter.restore_stds() - - def open_external_console(self, fname, wdir, args, interact, debug, python, - python_args, systerm): - """Open external console""" - if systerm: - # Running script in an external system terminal - try: - programs.run_python_script_in_terminal(fname, wdir, args, - interact, debug, python_args) - except NotImplementedError: - QMessageBox.critical(self, _("Run"), - _("Running an external system terminal " - "is not supported on platform %s." - ) % os.name) - else: - self.extconsole.visibility_changed(True) - self.extconsole.raise_() - self.extconsole.start( - fname=to_text_string(fname), wdir=to_text_string(wdir), - args=to_text_string(args), interact=interact, - debug=debug, python=python, - python_args=to_text_string(python_args) ) - - def execute_in_external_console(self, lines, focus_to_editor): - """ - Execute lines in external or IPython console and eventually set focus - to the editor - """ - console = self.extconsole - if self.ipyconsole is None or self.last_console_plugin_focus_was_python: - console = self.extconsole - else: - console = self.ipyconsole - console.visibility_changed(True) - console.raise_() - console.execute_python_code(lines) - if focus_to_editor: - self.editor.visibility_changed(True) - - def new_file(self, text): - self.editor.new(text=text) - - def open_file(self, fname, external=False): - """ - Open filename with the appropriate application - Redirect to the right widget (txt -> editor, spydata -> workspace, ...) - or open file outside Spyder (if extension is not supported) - """ - fname = to_text_string(fname) - ext = osp.splitext(fname)[1] - if ext in EDIT_EXT: - self.editor.load(fname) - elif self.variableexplorer is not None and ext in IMPORT_EXT: - self.variableexplorer.import_data(fname) - elif encoding.is_text_file(fname): - self.editor.load(fname) - elif not external: - fname = file_uri(fname) - programs.start_file(fname) - - def open_external_file(self, fname): - """ - Open external files that can be handled either by the Editor or the - variable explorer inside Spyder. - """ - fname = encoding.to_unicode_from_fs(fname) - if osp.isfile(fname): - self.open_file(fname, external=True) - elif osp.isfile(osp.join(CWD, fname)): - self.open_file(osp.join(CWD, fname), external=True) - - #---- PYTHONPATH management, etc. - def get_spyder_pythonpath(self): - """Return Spyder PYTHONPATH""" - return self.path+self.project_path - - def add_path_to_sys_path(self): - """Add Spyder path to sys.path""" - for path in reversed(self.get_spyder_pythonpath()): - sys.path.insert(1, path) - - def remove_path_from_sys_path(self): - """Remove Spyder path from sys.path""" - sys_path = sys.path - while sys_path[1] in self.get_spyder_pythonpath(): - sys_path.pop(1) - - def path_manager_callback(self): - """Spyder path manager""" - from spyderlib.widgets.pathmanager import PathManager - self.remove_path_from_sys_path() - project_pathlist = self.projectexplorer.get_pythonpath() - dialog = PathManager(self, self.path, project_pathlist, sync=True) - self.connect(dialog, SIGNAL('redirect_stdio(bool)'), - self.redirect_internalshell_stdio) - dialog.exec_() - self.add_path_to_sys_path() - encoding.writelines(self.path, self.SPYDER_PATH) # Saving path - self.emit(SIGNAL("pythonpath_changed()")) - - def pythonpath_changed(self): - """Project Explorer PYTHONPATH contribution has changed""" - self.remove_path_from_sys_path() - self.project_path = self.projectexplorer.get_pythonpath() - self.add_path_to_sys_path() - self.emit(SIGNAL("pythonpath_changed()")) - - def win_env(self): - """Show Windows current user environment variables""" - self.dialog_manager.show(WinUserEnvDialog(self)) - - #---- Preferences - def apply_settings(self): - """Apply settings changed in 'Preferences' dialog box""" - qapp = QApplication.instance() - # Set 'gtk+' as the default theme in Gtk-based desktops - # Fixes Issue 2036 - if is_gtk_desktop() and ('GTK+' in QStyleFactory.keys()): - try: - qapp.setStyle('gtk+') - except: - pass - else: - qapp.setStyle(CONF.get('main', 'windows_style', - self.default_style)) - - default = self.DOCKOPTIONS - if CONF.get('main', 'vertical_tabs'): - default = default|QMainWindow.VerticalTabs - if CONF.get('main', 'animated_docks'): - default = default|QMainWindow.AnimatedDocks - self.setDockOptions(default) - - for child in self.widgetlist: - features = child.FEATURES - if CONF.get('main', 'vertical_dockwidget_titlebars'): - features = features|QDockWidget.DockWidgetVerticalTitleBar - child.dockwidget.setFeatures(features) - child.update_margins() - - self.apply_statusbar_settings() - - def apply_statusbar_settings(self): - """Update status bar widgets settings""" - for widget, name in ((self.mem_status, 'memory_usage'), - (self.cpu_status, 'cpu_usage')): - if widget is not None: - widget.setVisible(CONF.get('main', '%s/enable' % name)) - widget.set_interval(CONF.get('main', '%s/timeout' % name)) - - def edit_preferences(self): - """Edit Spyder preferences""" - from spyderlib.plugins.configdialog import ConfigDialog - dlg = ConfigDialog(self) - self.connect(dlg, SIGNAL("size_change(QSize)"), - lambda s: self.set_prefs_size(s)) - if self.prefs_dialog_size is not None: - dlg.resize(self.prefs_dialog_size) - for PrefPageClass in self.general_prefs: - widget = PrefPageClass(dlg, main=self) - widget.initialize() - dlg.add_page(widget) - for plugin in [self.workingdirectory, self.editor, - self.projectexplorer, self.extconsole, self.ipyconsole, - self.historylog, self.inspector, self.variableexplorer, - self.onlinehelp, self.explorer, self.findinfiles - ]+self.thirdparty_plugins: - if plugin is not None: - widget = plugin.create_configwidget(dlg) - if widget is not None: - dlg.add_page(widget) - if self.prefs_index is not None: - dlg.set_current_index(self.prefs_index) - dlg.show() - dlg.check_all_settings() - self.connect(dlg.pages_widget, SIGNAL("currentChanged(int)"), - self.__preference_page_changed) - dlg.exec_() - - def __preference_page_changed(self, index): - """Preference page index has changed""" - self.prefs_index = index - - def set_prefs_size(self, size): - """Save preferences dialog size""" - self.prefs_dialog_size = size - - #---- Shortcuts - def register_shortcut(self, qaction_or_qshortcut, context, name, - default=NoDefault): - """ - Register QAction or QShortcut to Spyder main application, - with shortcut (context, name, default) - """ - self.shortcut_data.append( (qaction_or_qshortcut, - context, name, default) ) - self.apply_shortcuts() - - def remove_deprecated_shortcuts(self): - """Remove deprecated shortcuts""" - data = [(context, name) for (qobject, context, name, - default) in self.shortcut_data] - remove_deprecated_shortcuts(data) - - def apply_shortcuts(self): - """Apply shortcuts settings to all widgets/plugins""" - toberemoved = [] - for index, (qobject, context, name, - default) in enumerate(self.shortcut_data): - keyseq = QKeySequence( get_shortcut(context, name, default) ) - try: - if isinstance(qobject, QAction): - qobject.setShortcut(keyseq) - elif isinstance(qobject, QShortcut): - qobject.setKey(keyseq) - except RuntimeError: - # Object has been deleted - toberemoved.append(index) - for index in sorted(toberemoved, reverse=True): - self.shortcut_data.pop(index) - - #---- Sessions - def load_session(self, filename=None): - """Load session""" - if filename is None: - self.redirect_internalshell_stdio(False) - filename, _selfilter = getopenfilename(self, _("Open session"), - getcwd(), _("Spyder sessions")+" (*.session.tar)") - self.redirect_internalshell_stdio(True) - if not filename: - return - if self.close(): - self.next_session_name = filename - - def save_session(self): - """Save session and quit application""" - self.redirect_internalshell_stdio(False) - filename, _selfilter = getsavefilename(self, _("Save session"), - getcwd(), _("Spyder sessions")+" (*.session.tar)") - self.redirect_internalshell_stdio(True) - if filename: - if self.close(): - self.save_session_name = filename - - def start_open_files_server(self): - self.open_files_server.setsockopt(socket.SOL_SOCKET, - socket.SO_REUSEADDR, 1) - port = select_port(default_port=OPEN_FILES_PORT) - CONF.set('main', 'open_files_port', port) - self.open_files_server.bind(('127.0.0.1', port)) - self.open_files_server.listen(20) - while 1: # 1 is faster than True - try: - req, dummy = self.open_files_server.accept() - except socket.error as e: - # See Issue 1275 for details on why errno EINTR is - # silently ignored here. - eintr = errno.WSAEINTR if os.name == 'nt' else errno.EINTR - # To avoid a traceback after closing on Windows - if e.args[0] == eintr: - continue - raise - fname = req.recv(1024) - if not self.light: - fname = fname.decode('utf-8') - self.emit(SIGNAL('open_external_file(QString)'), fname) - req.sendall(b' ') - - -#============================================================================== -# Utilities to create the 'main' function -#============================================================================== -def initialize(): - """Initialize Qt, patching sys.exit and eventually setting up ETS""" - # This doesn't create our QApplication, just holds a reference to - # MAIN_APP, created above to show our splash screen as early as - # possible - app = qapplication() - - #----Monkey patching PyQt4.QtGui.QApplication - class FakeQApplication(QApplication): - """Spyder's fake QApplication""" - def __init__(self, args): - self = app # analysis:ignore - @staticmethod - def exec_(): - """Do nothing because the Qt mainloop is already running""" - pass - from spyderlib.qt import QtGui - QtGui.QApplication = FakeQApplication - - #----Monkey patching rope - try: - from spyderlib import rope_patch - rope_patch.apply() - except ImportError: - # rope 0.9.2/0.9.3 is not installed - pass - - #----Monkey patching sys.exit - def fake_sys_exit(arg=[]): - pass - sys.exit = fake_sys_exit - - # Removing arguments from sys.argv as in standard Python interpreter - sys.argv = [''] - - # Selecting Qt4 backend for Enthought Tool Suite (if installed) - try: - from enthought.etsconfig.api import ETSConfig - ETSConfig.toolkit = 'qt4' - except ImportError: - pass - - #----Monkey patching rope (if installed) - # Compatibility with new Mercurial API (>= 1.3). - # New versions of rope (> 0.9.2) already handle this issue - try: - import rope - if rope.VERSION == '0.9.2': - import rope.base.fscommands - - class MercurialCommands(rope.base.fscommands.MercurialCommands): - def __init__(self, root): - self.hg = self._import_mercurial() - self.normal_actions = rope.base.fscommands.FileSystemCommands() - try: - self.ui = self.hg.ui.ui( - verbose=False, debug=False, quiet=True, - interactive=False, traceback=False, - report_untrusted=False) - except: - self.ui = self.hg.ui.ui() - self.ui.setconfig('ui', 'interactive', 'no') - self.ui.setconfig('ui', 'debug', 'no') - self.ui.setconfig('ui', 'traceback', 'no') - self.ui.setconfig('ui', 'verbose', 'no') - self.ui.setconfig('ui', 'report_untrusted', 'no') - self.ui.setconfig('ui', 'quiet', 'yes') - self.repo = self.hg.hg.repository(self.ui, root) - - rope.base.fscommands.MercurialCommands = MercurialCommands - except ImportError: - pass - - return app - - -class Spy(object): - """ - Inspect Spyder internals - - Attributes: - app Reference to main QApplication object - window Reference to spyder.MainWindow widget - """ - def __init__(self, app, window): - self.app = app - self.window = window - def __dir__(self): - return list(self.__dict__.keys()) +\ - [x for x in dir(self.__class__) if x[0] != '_'] - def versions(self): - return get_versions() - - -def run_spyder(app, options, args): - """ - Create and show Spyder's main window - Start QApplication event loop - """ - #TODO: insert here - # Main window - main = MainWindow(options) - try: - main.setup() - except BaseException: - if main.console is not None: - try: - main.console.shell.exit_interpreter() - except BaseException: - pass - raise - - main.show() - main.post_visible_setup() - - if main.console: - main.console.shell.interpreter.namespace['spy'] = \ - Spy(app=app, window=main) - - # Open external files passed as args - if args: - for a in args: - main.open_external_file(a) - - # Don't show icons in menus for Mac - if sys.platform == 'darwin': - QCoreApplication.setAttribute(Qt.AA_DontShowIconsInMenus, True) - - # Open external files with our Mac app - if running_in_mac_app(): - main.connect(app, SIGNAL('open_external_file(QString)'), - lambda fname: main.open_external_file(fname)) - - # To give focus again to the last focused widget after restoring - # the window - main.connect(app, SIGNAL('focusChanged(QWidget*, QWidget*)'), - main.change_last_focused_widget) - - app.exec_() - return main - - -def __remove_temp_session(): - if osp.isfile(TEMP_SESSION_PATH): - os.remove(TEMP_SESSION_PATH) - - -#============================================================================== -# Main -#============================================================================== -def main(): - """Session manager""" - __remove_temp_session() - - # **** Collect command line options **** - # Note regarding Options: - # It's important to collect options before monkey patching sys.exit, - # otherwise, optparse won't be able to exit if --help option is passed - options, args = get_options() - - if set_attached_console_visible is not None: - set_attached_console_visible(DEBUG or options.show_console\ - or options.reset_session\ - or options.reset_to_defaults\ - or options.optimize) - - app = initialize() - if options.reset_session: - # Remove all configuration files! - reset_session() -# CONF.reset_to_defaults(save=True) - return - elif options.reset_to_defaults: - # Reset Spyder settings to defaults - CONF.reset_to_defaults(save=True) - return - elif options.optimize: - # Optimize the whole Spyder's source code directory - import spyderlib - programs.run_python_script(module="compileall", - args=[spyderlib.__path__[0]], p_args=['-O']) - return - - if CONF.get('main', 'crash', False): - CONF.set('main', 'crash', False) - SPLASH.hide() - QMessageBox.information(None, "Spyder", - "Spyder crashed during last session.

    " - "If Spyder does not start at all and before submitting a " - "bug report, please try to reset settings to defaults by " - "running Spyder with the command line option '--reset':
    " - "python spyder --reset" - "

    " - "Warning: " - "this command will remove all your Spyder configuration files " - "located in '%s').

    " - "If restoring the default settings does not help, please take " - "the time to search for known bugs or " - "discussions matching your situation before " - "eventually creating a new issue here. " - "Your feedback will always be greatly appreciated." - "" % (get_conf_path(), __project_url__, - __forum_url__, __project_url__)) - - next_session_name = options.startup_session - while is_text_string(next_session_name): - if next_session_name: - error_message = load_session(next_session_name) - if next_session_name == TEMP_SESSION_PATH: - __remove_temp_session() - if error_message is None: - CONF.load_from_ini() - else: - print(error_message) - QMessageBox.critical(None, "Load session", - u("Unable to load '%s'

    Error message:
    %s") - % (osp.basename(next_session_name), error_message)) - mainwindow = None - try: - mainwindow = run_spyder(app, options, args) - except BaseException: - CONF.set('main', 'crash', True) - import traceback - traceback.print_exc(file=STDERR) - traceback.print_exc(file=open('spyder_crash.log', 'w')) - if mainwindow is None: - # An exception occured - SPLASH.hide() - return - next_session_name = mainwindow.next_session_name - save_session_name = mainwindow.save_session_name - if next_session_name is not None: - #-- Loading session - # Saving current session in a temporary file - # but only if we are not currently trying to reopen it! - if next_session_name != TEMP_SESSION_PATH: - save_session_name = TEMP_SESSION_PATH - if save_session_name: - #-- Saving session - error_message = save_session(save_session_name) - if error_message is not None: - QMessageBox.critical(None, "Save session", - u("Unable to save '%s'

    Error message:
    %s") - % (osp.basename(save_session_name), error_message)) - ORIGINAL_SYS_EXIT() - - -if __name__ == "__main__": - main() diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/start_app.py spyder-3.0.2+dfsg1/spyder/start_app.py --- spyder-2.3.8+dfsg1/spyder/start_app.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/start_app.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,130 +0,0 @@ -# -*- coding: utf-8 -*- - -import os -import os.path as osp -import socket -import time -import atexit -import random - -# Local imports -from spyderlib.cli_options import get_options -from spyderlib.baseconfig import get_conf_path, running_in_mac_app -from spyderlib.config import CONF -from spyderlib.baseconfig import DEV, TEST -from spyderlib.utils.external import lockfile -from spyderlib.py3compat import is_unicode - - -def send_args_to_spyder(args): - """ - Simple socket client used to send the args passed to the Spyder - executable to an already running instance. - - Args can be Python scripts or files with these extensions: .spydata, .mat, - .npy, or .h5, which can be imported by the Variable Explorer. - """ - port = CONF.get('main', 'open_files_port') - - # Wait ~50 secs for the server to be up - # Taken from http://stackoverflow.com/a/4766598/438386 - for _x in range(200): - try: - for arg in args: - client = socket.socket(socket.AF_INET, socket.SOCK_STREAM, - socket.IPPROTO_TCP) - client.connect(("127.0.0.1", port)) - if is_unicode(arg): - arg = arg.encode('utf-8') - client.send(osp.abspath(arg)) - client.close() - except socket.error: - time.sleep(0.25) - continue - break - - -def main(): - """ - Start Spyder application. - - If single instance mode is turned on (default behavior) and an instance of - Spyder is already running, this will just parse and send command line - options to the application. - """ - # Renaming old configuration files (the '.' prefix has been removed) - # (except for .spyder.ini --> spyder.ini, which is done in userconfig.py) - if DEV is None: - cpath = get_conf_path() - for fname in os.listdir(cpath): - if fname.startswith('.'): - old, new = osp.join(cpath, fname), osp.join(cpath, fname[1:]) - try: - os.rename(old, new) - except OSError: - pass - - # Parse command line options - options, args = get_options() - - if CONF.get('main', 'single_instance') and not options.new_instance \ - and not running_in_mac_app(): - # Minimal delay (0.1-0.2 secs) to avoid that several - # instances started at the same time step in their - # own foots while trying to create the lock file - time.sleep(random.randrange(1000, 2000, 90)/10000.) - - # Lock file creation - lock_file = get_conf_path('spyder.lock') - lock = lockfile.FilesystemLock(lock_file) - - # Try to lock spyder.lock. If it's *possible* to do it, then - # there is no previous instance running and we can start a - # new one. If *not*, then there is an instance already - # running, which is locking that file - try: - lock_created = lock.lock() - except: - # If locking fails because of errors in the lockfile - # module, try to remove a possibly stale spyder.lock. - # This is reported to solve all problems with - # lockfile (See issue 2363) - try: - if os.name == 'nt': - if osp.isdir(lock_file): - import shutil - shutil.rmtree(lock_file, ignore_errors=True) - else: - if osp.islink(lock_file): - os.unlink(lock_file) - except: - pass - - # Then start Spyder as usual and *don't* continue - # executing this script because it doesn't make - # sense - from spyderlib import spyder - spyder.main() - return - - if lock_created: - # Start a new instance - if TEST is None: - atexit.register(lock.unlock) - from spyderlib import spyder - spyder.main() - else: - # Pass args to Spyder or print an informative - # message - if args: - send_args_to_spyder(args) - else: - print("Spyder is already running. If you want to open a new \n" - "instance, please pass to it the --new-instance option") - else: - from spyderlib import spyder - spyder.main() - - -if __name__ == "__main__": - main() diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/userconfig.py spyder-3.0.2+dfsg1/spyder/userconfig.py --- spyder-2.3.8+dfsg1/spyder/userconfig.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/userconfig.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,454 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# userconfig License Agreement (MIT License) -# ------------------------------------------ -# -# Copyright © 2009-2012 Pierre Raybaut -# Copyright © 2014 The Spyder Development Team -# -# Permission is hereby granted, free of charge, to any person -# obtaining a copy of this software and associated documentation -# files (the "Software"), to deal in the Software without -# restriction, including without limitation the rights to use, -# copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following -# conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -# OTHER DEALINGS IN THE SOFTWARE. - - -""" -This module provides user configuration file (.ini file) management features -based on ``ConfigParser`` (present in the standard library). -""" - -from __future__ import print_function - -import os -import re -import os.path as osp -import shutil -import time - -from spyderlib.baseconfig import (DEV, TEST, get_module_source_path, - get_home_dir) -from spyderlib.utils.programs import check_version -from spyderlib.py3compat import configparser as cp -from spyderlib.py3compat import PY2, is_text_string, to_text_string - -if PY2: - import codecs - -#============================================================================== -# Auxiliary classes -#============================================================================== -class NoDefault: - pass - - -#============================================================================== -# Defaults class -#============================================================================== -class DefaultsConfig(cp.ConfigParser): - """ - Class used to save defaults to a file and as base class for - UserConfig - """ - def __init__(self, name, subfolder): - cp.ConfigParser.__init__(self) - self.name = name - self.subfolder = subfolder - - def _write(self, fp): - """ - Private write method for Python 2 - The one from configparser fails for non-ascii Windows accounts - """ - if self._defaults: - fp.write("[%s]\n" % DEFAULTSECT) - for (key, value) in self._defaults.items(): - fp.write("%s = %s\n" % (key, str(value).replace('\n', '\n\t'))) - fp.write("\n") - for section in self._sections: - fp.write("[%s]\n" % section) - for (key, value) in self._sections[section].items(): - if key == "__name__": - continue - if (value is not None) or (self._optcre == self.OPTCRE): - value = to_text_string(value) - key = " = ".join((key, value.replace('\n', '\n\t'))) - fp.write("%s\n" % (key)) - fp.write("\n") - - def _set(self, section, option, value, verbose): - """ - Private set method - """ - if not self.has_section(section): - self.add_section( section ) - if not is_text_string(value): - value = repr( value ) - if verbose: - print('%s[ %s ] = %s' % (section, option, value)) - cp.ConfigParser.set(self, section, option, value) - - def _save(self): - """ - Save config into the associated .ini file - """ - # See Issue 1086 and 1242 for background on why this - # method contains all the exception handling. - fname = self.filename() - - def _write_file(fname): - if PY2: - # Python 2 - with codecs.open(fname, 'w', encoding='utf-8') as configfile: - self._write(configfile) - else: - # Python 3 - with open(fname, 'w', encoding='utf-8') as configfile: - self.write(configfile) - - try: # the "easy" way - _write_file(fname) - except IOError: - try: # the "delete and sleep" way - if osp.isfile(fname): - os.remove(fname) - time.sleep(0.05) - _write_file(fname) - except Exception as e: - print("Failed to write user configuration file.") - print("Please submit a bug report.") - raise(e) - - def filename(self): - """ - Create a .ini filename located in user home directory - """ - if TEST is None: - folder = get_home_dir() - else: - import tempfile - folder = tempfile.gettempdir() - w_dot = osp.join(folder, '.%s.ini' % self.name) - if self.subfolder is None: - return w_dot - else: - folder = osp.join(folder, self.subfolder) - w_dot = osp.join(folder, '.%s.ini' % self.name) - # Save defaults in a "defaults" dir of .spyder2 to not pollute it - if 'defaults' in self.name: - folder = osp.join(folder, 'defaults') - try: - os.makedirs(folder) - except os.error: - # Folder (or one of its parents) already exists - pass - old, new = w_dot, osp.join(folder, '%s.ini' % self.name) - if osp.isfile(old) and DEV is None: - try: - if osp.isfile(new): - os.remove(old) - else: - os.rename(old, new) - except OSError: - pass - return new - - def set_defaults(self, defaults): - for section, options in defaults: - for option in options: - new_value = options[ option ] - self._set(section, option, new_value, False) - - -#============================================================================== -# User config class -#============================================================================== -class UserConfig(DefaultsConfig): - """ - UserConfig class, based on ConfigParser - name: name of the config - defaults: dictionnary containing options - *or* list of tuples (section_name, options) - version: version of the configuration file (X.Y.Z format) - subfolder: configuration file will be saved in %home%/subfolder/%name%.ini - - Note that 'get' and 'set' arguments number and type - differ from the overriden methods - """ - DEFAULT_SECTION_NAME = 'main' - def __init__(self, name, defaults=None, load=True, version=None, - subfolder=None, backup=False, raw_mode=False, - remove_obsolete=False): - DefaultsConfig.__init__(self, name, subfolder) - self.raw = 1 if raw_mode else 0 - if (version is not None) and (re.match('^(\d+).(\d+).(\d+)$', version) is None): - raise ValueError("Version number %r is incorrect - must be in X.Y.Z format" % version) - if isinstance(defaults, dict): - defaults = [ (self.DEFAULT_SECTION_NAME, defaults) ] - self.defaults = defaults - if defaults is not None: - self.reset_to_defaults(save=False) - fname = self.filename() - if backup: - try: - shutil.copyfile(fname, "%s.bak" % fname) - except IOError: - pass - if load: - # If config file already exists, it overrides Default options: - self.load_from_ini() - old_ver = self.get_version(version) - _major = lambda _t: _t[:_t.find('.')] - _minor = lambda _t: _t[:_t.rfind('.')] - # Save new defaults - self.__save_new_defaults(defaults, version, subfolder) - # Updating defaults only if major/minor version is different - if _minor(version) != _minor(old_ver): - if backup: - try: - shutil.copyfile(fname, "%s-%s.bak" % (fname, old_ver)) - except IOError: - pass - if check_version(old_ver, '2.4.0', '<'): - self.reset_to_defaults(save=False) - else: - self.__update_defaults(defaults, old_ver) - # Remove deprecated options if major version has changed - if remove_obsolete or _major(version) != _major(old_ver): - self.__remove_deprecated_options(old_ver) - # Set new version number - self.set_version(version, save=False) - if defaults is None: - # If no defaults are defined, set .ini file settings as default - self.set_as_defaults() - - def get_version(self, version='0.0.0'): - """Return configuration (not application!) version""" - return self.get(self.DEFAULT_SECTION_NAME, 'version', version) - - def set_version(self, version='0.0.0', save=True): - """Set configuration (not application!) version""" - self.set(self.DEFAULT_SECTION_NAME, 'version', version, save=save) - - def load_from_ini(self): - """ - Load config from the associated .ini file - """ - try: - if PY2: - # Python 2 - fname = self.filename() - if osp.isfile(fname): - try: - with codecs.open(fname, encoding='utf-8') as configfile: - self.readfp(configfile) - except IOError: - print("Failed reading file", fname) - else: - # Python 3 - self.read(self.filename(), encoding='utf-8') - except cp.MissingSectionHeaderError: - print("Warning: File contains no section headers.") - - def __load_old_defaults(self, old_version): - """Read old defaults""" - old_defaults = cp.ConfigParser() - if check_version(old_version, '3.0.0', '<='): - path = get_module_source_path('spyderlib') - else: - path = osp.dirname(self.filename()) - path = osp.join(path, 'defaults') - old_defaults.read(osp.join(path, 'defaults-'+old_version+'.ini')) - return old_defaults - - def __save_new_defaults(self, defaults, new_version, subfolder): - """Save new defaults""" - new_defaults = DefaultsConfig(name='defaults-'+new_version, - subfolder=subfolder) - if not osp.isfile(new_defaults.filename()): - new_defaults.set_defaults(defaults) - new_defaults._save() - - def __update_defaults(self, defaults, old_version, verbose=False): - """Update defaults after a change in version""" - old_defaults = self.__load_old_defaults(old_version) - for section, options in defaults: - for option in options: - new_value = options[ option ] - try: - old_value = old_defaults.get(section, option) - except (cp.NoSectionError, cp.NoOptionError): - old_value = None - if old_value is None or \ - to_text_string(new_value) != old_value: - self._set(section, option, new_value, verbose) - - def __remove_deprecated_options(self, old_version): - """ - Remove options which are present in the .ini file but not in defaults - """ - old_defaults = self.__load_old_defaults(old_version) - for section in old_defaults.sections(): - for option, _ in old_defaults.items(section, raw=self.raw): - if self.get_default(section, option) is NoDefault: - self.remove_option(section, option) - if len(self.items(section, raw=self.raw)) == 0: - self.remove_section(section) - - def cleanup(self): - """ - Remove .ini file associated to config - """ - os.remove(self.filename()) - - def set_as_defaults(self): - """ - Set defaults from the current config - """ - self.defaults = [] - for section in self.sections(): - secdict = {} - for option, value in self.items(section, raw=self.raw): - secdict[option] = value - self.defaults.append( (section, secdict) ) - - def reset_to_defaults(self, save=True, verbose=False, section=None): - """ - Reset config to Default values - """ - for sec, options in self.defaults: - if section == None or section == sec: - for option in options: - value = options[ option ] - self._set(sec, option, value, verbose) - if save: - self._save() - - def __check_section_option(self, section, option): - """ - Private method to check section and option types - """ - if section is None: - section = self.DEFAULT_SECTION_NAME - elif not is_text_string(section): - raise RuntimeError("Argument 'section' must be a string") - if not is_text_string(option): - raise RuntimeError("Argument 'option' must be a string") - return section - - def get_default(self, section, option): - """ - Get Default value for a given (section, option) - -> useful for type checking in 'get' method - """ - section = self.__check_section_option(section, option) - for sec, options in self.defaults: - if sec == section: - if option in options: - return options[ option ] - else: - return NoDefault - - def get(self, section, option, default=NoDefault): - """ - Get an option - section=None: attribute a default section name - default: default value (if not specified, an exception - will be raised if option doesn't exist) - """ - section = self.__check_section_option(section, option) - - if not self.has_section(section): - if default is NoDefault: - raise cp.NoSectionError(section) - else: - self.add_section(section) - - if not self.has_option(section, option): - if default is NoDefault: - raise cp.NoOptionError(option, section) - else: - self.set(section, option, default) - return default - - value = cp.ConfigParser.get(self, section, option, raw=self.raw) - default_value = self.get_default(section, option) - if isinstance(default_value, bool): - value = eval(value) - elif isinstance(default_value, float): - value = float(value) - elif isinstance(default_value, int): - value = int(value) - else: - if PY2 and is_text_string(default_value): - try: - value = value.decode('utf-8') - except (UnicodeEncodeError, UnicodeDecodeError): - pass - try: - # lists, tuples, ... - value = eval(value) - except: - pass - return value - - def set_default(self, section, option, default_value): - """ - Set Default value for a given (section, option) - -> called when a new (section, option) is set and no default exists - """ - section = self.__check_section_option(section, option) - for sec, options in self.defaults: - if sec == section: - options[ option ] = default_value - - def set(self, section, option, value, verbose=False, save=True): - """ - Set an option - section=None: attribute a default section name - """ - section = self.__check_section_option(section, option) - default_value = self.get_default(section, option) - if default_value is NoDefault: - # This let us save correctly string value options with - # no config default that contain non-ascii chars in - # Python 2 - if PY2 and is_text_string(value): - value = repr(value) - default_value = value - self.set_default(section, option, default_value) - if isinstance(default_value, bool): - value = bool(value) - elif isinstance(default_value, float): - value = float(value) - elif isinstance(default_value, int): - value = int(value) - elif not is_text_string(default_value): - value = repr(value) - self._set(section, option, value, verbose) - if save: - self._save() - - def remove_section(self, section): - cp.ConfigParser.remove_section(self, section) - self._save() - - def remove_option(self, section, option): - cp.ConfigParser.remove_option(self, section, option) - self._save() diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/utils/bsdsocket.py spyder-3.0.2+dfsg1/spyder/utils/bsdsocket.py --- spyder-2.3.8+dfsg1/spyder/utils/bsdsocket.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/utils/bsdsocket.py 2016-10-25 02:05:23.000000000 +0200 @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- # -# Copyright © 2011 Pierre Raybaut +# Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) +# (see spyder/__init__.py for details) """BSD socket interface communication utilities""" @@ -18,9 +18,9 @@ import traceback # Local imports -from spyderlib.baseconfig import DEBUG, STDERR +from spyder.config.base import DEBUG, STDERR DEBUG_EDITOR = DEBUG >= 3 -from spyderlib.py3compat import pickle +from spyder.py3compat import pickle PICKLE_HIGHEST_PROTOCOL = 2 @@ -141,43 +141,42 @@ if __name__ == '__main__': - # socket read/write testing - client and server in one thread - - # (techtonik): the stuff below is placed into public domain - print("-- Testing standard Python socket interface --") - - address = ("127.0.0.1", 9999) - - server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - server.setblocking(0) - server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - server.bind( address ) - server.listen(2) - - client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - client.connect( address ) - - client.send("data to be catched") - # accepted server socket is the one we can read from - # note that it is different from server socket - accsock, addr = server.accept() - print('..got "%s" from %s' % (accsock.recv(4096), addr)) - client.send("more data for recv") - print('..got "%s" from %s' % (accsock.recv(4096), addr)) - - # accsock.close() - # client.send("more data for recv") - #socket.error: [Errno 9] Bad file descriptor - # accsock, addr = server.accept() - #socket.error: [Errno 11] Resource temporarily unavailable - - - print("-- Testing BSD socket write_packet/read_packet --") - - write_packet(client, "a tiny piece of data") - print('..got "%s" from read_packet()' % (read_packet(accsock))) - - client.close() - server.close() - - print("-- Done.") + if not os.name == 'nt': + # socket read/write testing - client and server in one thread + + # (techtonik): the stuff below is placed into public domain + print("-- Testing standard Python socket interface --") + + address = ("127.0.0.1", 9999) + + server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + server.setblocking(0) + server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + server.bind( address ) + server.listen(2) + + client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + client.connect( address ) + + client.send("data to be catched".encode('utf-8')) + # accepted server socket is the one we can read from + # note that it is different from server socket + accsock, addr = server.accept() + print('..got "%s" from %s' % (accsock.recv(4096), addr)) + + # accsock.close() + # client.send("more data for recv") + #socket.error: [Errno 9] Bad file descriptor + # accsock, addr = server.accept() + #socket.error: [Errno 11] Resource temporarily unavailable + + + print("-- Testing BSD socket write_packet/read_packet --") + + write_packet(client, "a tiny piece of data") + print('..got "%s" from read_packet()' % (read_packet(accsock))) + + client.close() + server.close() + + print("-- Done.") diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/utils/codeanalysis.py spyder-3.0.2+dfsg1/spyder/utils/codeanalysis.py --- spyder-2.3.8+dfsg1/spyder/utils/codeanalysis.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/utils/codeanalysis.py 2016-10-25 02:05:23.000000000 +0200 @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- # -# Copyright © 2011 Pierre Raybaut +# Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) +# (see spyder/__init__.py for details) """ Source code analysis utilities @@ -11,15 +11,14 @@ import sys import re import os -from subprocess import Popen, PIPE import tempfile import traceback # Local import -from spyderlib.baseconfig import _, DEBUG -from spyderlib.utils import programs, encoding -from spyderlib.py3compat import to_text_string, to_binary_string, PY3 -from spyderlib import dependencies +from spyder.config.base import _, DEBUG +from spyder.utils import programs, encoding +from spyder.py3compat import to_text_string, to_binary_string, PY3 +from spyder import dependencies DEBUG_EDITOR = DEBUG >= 3 #============================================================================== @@ -79,7 +78,7 @@ results.append((warning.message % warning.message_args, warning.lineno)) except Exception: - # Never return None to avoid lock in spyderlib/widgets/editor.py + # Never return None to avoid lock in spyder/widgets/editor.py # See Issue 1547 results = [] if DEBUG_EDITOR: @@ -144,8 +143,10 @@ args.append(tempfd.name) else: args.append(filename) - output = Popen(args, stdout=PIPE, stderr=PIPE - ).communicate()[0].strip().decode().splitlines() + cmd = args[0] + cmdargs = args[1:] + proc = programs.run_program(cmd, cmdargs) + output = proc.communicate()[0].strip().decode().splitlines() if filename is None: os.unlink(tempfd.name) results = [] @@ -153,7 +154,11 @@ lines = source_code.splitlines() for line in output: lineno = int(re.search(r'(\:[\d]+\:)', line).group()[1:-1]) - if 'analysis:ignore' not in to_text_string(lines[lineno-1], coding): + try: + text = to_text_string(lines[lineno-1], coding) + except TypeError: + text = to_text_string(lines[lineno-1]) + if 'analysis:ignore' not in text: message = line[line.find(': ')+2:] results.append((message, lineno)) return results @@ -165,7 +170,7 @@ args = get_checker_executable('pep8') results = check(args, source_code, filename=filename, options=['-r']) except Exception: - # Never return None to avoid lock in spyderlib/widgets/editor.py + # Never return None to avoid lock in spyder/widgets/editor.py # See Issue 1547 results = [] if DEBUG_EDITOR: diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/utils/debug.py spyder-3.0.2+dfsg1/spyder/utils/debug.py --- spyder-2.3.8+dfsg1/spyder/utils/debug.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/utils/debug.py 2016-10-25 02:05:23.000000000 +0200 @@ -1,15 +1,14 @@ # -*- coding: utf-8 -*- # -# Copyright © 2009-2013 Pierre Raybaut -# Copyright © 2012-2013 anatoly techtonik +# Copyright © Spyder Project Contributors # # Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) +# (see spyder/__init__.py for details) """ Debug utilities that are independent of Spyder code. -See spyderlib.baseconfig for other helpers. +See spyder.config.base for other helpers. """ from __future__ import print_function diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/utils/dochelpers.py spyder-3.0.2+dfsg1/spyder/utils/dochelpers.py --- spyder-2.3.8+dfsg1/spyder/utils/dochelpers.py 2015-08-24 21:09:44.000000000 +0200 +++ spyder-3.0.2+dfsg1/spyder/utils/dochelpers.py 2016-10-25 02:05:23.000000000 +0200 @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- # -# Copyright © 2009-2010 Pierre Raybaut +# Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) +# (see spyder/__init__.py for details) """Utilities and wrappers around inspect module""" @@ -12,10 +12,10 @@ import re # Local imports: -from spyderlib.utils import encoding -from spyderlib.py3compat import (is_text_string, builtins, get_meth_func, - get_meth_class_inst, get_meth_class, - get_func_defaults, to_text_string) +from spyder.utils import encoding +from spyder.py3compat import (is_text_string, builtins, get_meth_func, + get_meth_class_inst, get_meth_class, + get_func_defaults, to_text_string) SYMBOLS = r"[^\'\"a-zA-Z0-9_.]" diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/utils/encoding.py spyder-3.0.2+dfsg1/spyder/utils/encoding.py --- spyder-2.3.8+dfsg1/spyder/utils/encoding.py 2015-08-24 21:09:44.000000000 +0200 +++ spyder-3.0.2+dfsg1/spyder/utils/encoding.py 2016-10-25 02:05:23.000000000 +0200 @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- # -# Copyright © 2009-2010 Pierre Raybaut +# Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) +# (see spyder/__init__.py for details) """ Text encoding utilities, text file I/O @@ -15,11 +15,11 @@ import os import locale import sys -from codecs import BOM_UTF8, BOM_UTF16, BOM_UTF32 +from codecs import BOM_UTF8, BOM_UTF16, BOM_UTF32, getincrementaldecoder # Local imports -from spyderlib.py3compat import (is_string, to_text_string, is_binary_string, - is_unicode) +from spyder.py3compat import (is_string, to_text_string, is_binary_string, + is_unicode) PREFERRED_ENCODING = locale.getpreferredencoding() @@ -91,10 +91,10 @@ # Codecs for working with files and text. CODING_RE = re.compile(r"coding[:=]\s*([-\w_.]+)") -CODECS = ['utf-8', 'iso8859-1', 'iso8859-15', 'koi8-r', - 'koi8-u', 'iso8859-2', 'iso8859-3', 'iso8859-4', 'iso8859-5', - 'iso8859-6', 'iso8859-7', 'iso8859-8', 'iso8859-9', - 'iso8859-10', 'iso8859-13', 'iso8859-14', 'latin-1', +CODECS = ['utf-8', 'iso8859-1', 'iso8859-15', 'ascii', 'koi8-r', + 'koi8-u', 'iso8859-2', 'iso8859-3', 'iso8859-4', 'iso8859-5', + 'iso8859-6', 'iso8859-7', 'iso8859-8', 'iso8859-9', + 'iso8859-10', 'iso8859-13', 'iso8859-14', 'latin-1', 'utf-16'] def get_coding(text): @@ -106,7 +106,10 @@ for line in text.splitlines()[:2]: result = CODING_RE.search(to_text_string(line)) if result: - return result.group(1) + codec = result.group(1) + # sometimes we find a false encoding that can result in errors + if codec in CODECS: + return codec return None def decode(text): @@ -243,13 +246,16 @@ for bom in [BOM_UTF8, BOM_UTF16, BOM_UTF32]: if chunk.startswith(bom): return True - chunk = chunk.decode('utf-8') + + decoder = getincrementaldecoder('utf-8')() while 1: + is_final = len(chunk) < CHUNKSIZE + chunk = decoder.decode(chunk, final=is_final) if '\0' in chunk: # found null byte return False - if len(chunk) < CHUNKSIZE: + if is_final: break # done - chunk = fid.read(CHUNKSIZE).decode('utf-8') + chunk = fid.read(CHUNKSIZE) except UnicodeDecodeError: return False except Exception: diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/utils/environ.py spyder-3.0.2+dfsg1/spyder/utils/environ.py --- spyder-2.3.8+dfsg1/spyder/utils/environ.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/utils/environ.py 2016-10-25 02:05:23.000000000 +0200 @@ -1,20 +1,23 @@ # -*- coding: utf-8 -*- # -# Copyright © 2009-2010 Pierre Raybaut +# Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) +# (see spyder/__init__.py for details) """ Environment variable utilities """ -from spyderlib.qt.QtGui import QDialog, QMessageBox - +# Standard library imports import os +# Third party imports +from qtpy.QtWidgets import QDialog, QMessageBox + # Local imports -from spyderlib.baseconfig import _ -from spyderlib.widgets.dicteditor import DictEditor +from spyder.config.base import _ +from spyder.utils import icon_manager as ima +from spyder.widgets.variableexplorer.collectionseditor import CollectionsEditor def envdict2listdict(envdict): @@ -25,6 +28,7 @@ envdict[key] = [path.strip() for path in envdict[key].split(sep)] return envdict + def listdict2envdict(listdict): """Dict of lists --> Dict""" for key in listdict: @@ -32,18 +36,20 @@ listdict[key] = os.path.pathsep.join(listdict[key]) return listdict -class RemoteEnvDialog(DictEditor): + +class RemoteEnvDialog(CollectionsEditor): """Remote process environment variables Dialog""" def __init__(self, get_environ_func, set_environ_func, parent=None): super(RemoteEnvDialog, self).__init__(parent) self.setup(envdict2listdict(get_environ_func()), - title="os.environ", width=600, icon='environ.png') + title="os.environ", width=600, icon=ima.icon('environ')) self.set_environ = set_environ_func def accept(self): """Reimplement Qt method""" self.set_environ(listdict2envdict(self.get_value())) QDialog.accept(self) + class EnvDialog(RemoteEnvDialog): """Environment variables Dialog""" def __init__(self): @@ -56,7 +62,7 @@ # For Windows only try: - from spyderlib.py3compat import winreg + from spyder.py3compat import winreg def get_user_env(): """Return HKCU (current user) environment variables""" @@ -95,8 +101,8 @@ _("Module pywin32 was not found.
    " "Please restart this Windows session " "(not the computer) for changes to take effect.")) - - class WinUserEnvDialog(DictEditor): + + class WinUserEnvDialog(CollectionsEditor): """Windows User Environment Variables Editor""" def __init__(self, parent=None): super(WinUserEnvDialog, self).__init__(parent) @@ -126,9 +132,12 @@ def main(): """Run Windows environment variable editor""" - from spyderlib.utils.qthelpers import qapplication + from spyder.utils.qthelpers import qapplication app = qapplication() - dialog = WinUserEnvDialog() + if os.name == 'nt': + dialog = WinUserEnvDialog() + else: + dialog = EnvDialog() dialog.show() app.exec_() diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/utils/external/__init__.py spyder-3.0.2+dfsg1/spyder/utils/external/__init__.py --- spyder-2.3.8+dfsg1/spyder/utils/external/__init__.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/utils/external/__init__.py 2016-10-25 02:05:23.000000000 +0200 @@ -1,12 +1,12 @@ # -*- coding: utf-8 -*- # -# Copyright © 2011 Pierre Raybaut +# Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) +# (see spyder/__init__.py for details) """ -spyderlib.utils.external -======================== +spyder.utils.external +===================== External libraries needed for Spyder to work. Put here only untouched libraries, else put them in utils. @@ -19,7 +19,7 @@ if os.name == 'nt': import os.path as osp import sys - from spyderlib.baseconfig import get_module_source_path + from spyder.config.base import get_module_source_path dirname = get_module_source_path(__name__) if osp.isdir(osp.join(dirname, 'rope')): diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/utils/external/lockfile.py spyder-3.0.2+dfsg1/spyder/utils/external/lockfile.py --- spyder-2.3.8+dfsg1/spyder/utils/external/lockfile.py 2015-08-24 21:09:44.000000000 +0200 +++ spyder-3.0.2+dfsg1/spyder/utils/external/lockfile.py 2016-11-07 20:00:18.000000000 +0100 @@ -1,6 +1,6 @@ # Copyright (c) 2005 Divmod, Inc. # Copyright (c) Twisted Matrix Laboratories. -# Copyright (c) 2013 The Spyder Development Team +# Copyright (c) Spyder Project Contributors # Twisted is distributed under the MIT license. """ @@ -16,7 +16,7 @@ from time import time as _uniquefloat -from spyderlib.py3compat import PY2, to_binary_string +from spyder.py3compat import PY2, to_binary_string def unique(): if PY2: diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/utils/external/path.py spyder-3.0.2+dfsg1/spyder/utils/external/path.py --- spyder-2.3.8+dfsg1/spyder/utils/external/path.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/utils/external/path.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,1047 +0,0 @@ -# -# Copyright (c) 2010 Mikhail Gusarov -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. -# - -""" path.py - An object representing a path to a file or directory. - -Original author: - Jason Orendorff - -Contributors: - Mikhail Gusarov - Marc Abramowitz - -Example: - -from path import path -d = path('/home/guido/bin') -for f in d.files('*.py'): - f.chmod(0755) - -This module requires Python 2.3 or later. -""" - -from __future__ import generators - -import sys -import warnings -import os -import fnmatch -import glob -import shutil -import codecs -import hashlib -import errno - -from spyderlib.py3compat import (TEXT_TYPES, getcwd, u, - is_text_string, is_unicode) - -__version__ = '2.4.1' -__all__ = ['path'] - -MODE_0777 = 511 -MODE_0666 = 438 - -# Platform-specific support for path.owner -if os.name == 'nt': - try: - import win32security - except ImportError: - win32security = None -else: - try: - import pwd - except ImportError: - pwd = None - -_base = TEXT_TYPES[-1] -_getcwd = getcwd - -# Universal newline support -_textmode = 'U' -if hasattr(__builtins__, 'open') and not hasattr(open, 'newlines'): - _textmode = 'r' - -class TreeWalkWarning(Warning): - pass - -class path(_base): - """ Represents a filesystem path. - - For documentation on individual methods, consult their - counterparts in os.path. - """ - - # --- Special Python methods. - - def __repr__(self): - return 'path(%s)' % _base.__repr__(self) - - # Adding a path and a string yields a path. - def __add__(self, more): - try: - resultStr = _base.__add__(self, more) - except TypeError: # Python bug - resultStr = NotImplemented - if resultStr is NotImplemented: - return resultStr - return self.__class__(resultStr) - - def __radd__(self, other): - if is_text_string(other): - return self.__class__(other.__add__(self)) - else: - return NotImplemented - - # The / operator joins paths. - def __div__(self, rel): - """ fp.__div__(rel) == fp / rel == fp.joinpath(rel) - - Join two path components, adding a separator character if - needed. - """ - return self.__class__(os.path.join(self, rel)) - - # Make the / operator work even when true division is enabled. - __truediv__ = __div__ - - def __enter__(self): - self._old_dir = self.getcwd() - os.chdir(self) - - def __exit__(self, *_): - os.chdir(self._old_dir) - - def getcwd(cls): - """ Return the current working directory as a path object. """ - return cls(_getcwd()) - getcwd = classmethod(getcwd) - - # - # --- Operations on path strings. - - def abspath(self): return self.__class__(os.path.abspath(self)) - def normcase(self): return self.__class__(os.path.normcase(self)) - def normpath(self): return self.__class__(os.path.normpath(self)) - def realpath(self): return self.__class__(os.path.realpath(self)) - def expanduser(self): return self.__class__(os.path.expanduser(self)) - def expandvars(self): return self.__class__(os.path.expandvars(self)) - def dirname(self): return self.__class__(os.path.dirname(self)) - def basename(self): return self.__class__(os.path.basename(self)) - - def expand(self): - """ Clean up a filename by calling expandvars(), - expanduser(), and normpath() on it. - - This is commonly everything needed to clean up a filename - read from a configuration file, for example. - """ - return self.expandvars().expanduser().normpath() - - def _get_namebase(self): - base, ext = os.path.splitext(self.name) - return base - - def _get_ext(self): - f, ext = os.path.splitext(_base(self)) - return ext - - def _get_drive(self): - drive, r = os.path.splitdrive(self) - return self.__class__(drive) - - parent = property( - dirname, None, None, - """ This path's parent directory, as a new path object. - - For example, path('/usr/local/lib/libpython.so').parent == path('/usr/local/lib') - """) - - name = property( - basename, None, None, - """ The name of this file or directory without the full path. - - For example, path('/usr/local/lib/libpython.so').name == 'libpython.so' - """) - - namebase = property( - _get_namebase, None, None, - """ The same as path.name, but with one file extension stripped off. - - For example, path('/home/guido/python.tar.gz').name == 'python.tar.gz', - but path('/home/guido/python.tar.gz').namebase == 'python.tar' - """) - - ext = property( - _get_ext, None, None, - """ The file extension, for example '.py'. """) - - drive = property( - _get_drive, None, None, - """ The drive specifier, for example 'C:'. - This is always empty on systems that don't use drive specifiers. - """) - - def splitpath(self): - """ p.splitpath() -> Return (p.parent, p.name). """ - parent, child = os.path.split(self) - return self.__class__(parent), child - - def splitdrive(self): - """ p.splitdrive() -> Return (p.drive, ). - - Split the drive specifier from this path. If there is - no drive specifier, p.drive is empty, so the return value - is simply (path(''), p). This is always the case on Unix. - """ - drive, rel = os.path.splitdrive(self) - return self.__class__(drive), rel - - def splitext(self): - """ p.splitext() -> Return (p.stripext(), p.ext). - - Split the filename extension from this path and return - the two parts. Either part may be empty. - - The extension is everything from '.' to the end of the - last path segment. This has the property that if - (a, b) == p.splitext(), then a + b == p. - """ - filename, ext = os.path.splitext(self) - return self.__class__(filename), ext - - def stripext(self): - """ p.stripext() -> Remove one file extension from the path. - - For example, path('/home/guido/python.tar.gz').stripext() - returns path('/home/guido/python.tar'). - """ - return self.splitext()[0] - - if hasattr(os.path, 'splitunc'): - def splitunc(self): - unc, rest = os.path.splitunc(self) - return self.__class__(unc), rest - - def _get_uncshare(self): - unc, r = os.path.splitunc(self) - return self.__class__(unc) - - uncshare = property( - _get_uncshare, None, None, - """ The UNC mount point for this path. - This is empty for paths on local drives. """) - - def joinpath(self, *args): - """ Join two or more path components, adding a separator - character (os.sep) if needed. Returns a new path - object. - """ - return self.__class__(os.path.join(self, *args)) - - def splitall(self): - r""" Return a list of the path components in this path. - - The first item in the list will be a path. Its value will be - either os.curdir, os.pardir, empty, or the root directory of - this path (for example, '/' or 'C:\\'). The other items in - the list will be strings. - - path.path.joinpath(*result) will yield the original path. - """ - parts = [] - loc = self - while loc != os.curdir and loc != os.pardir: - prev = loc - loc, child = prev.splitpath() - if loc == prev: - break - parts.append(child) - parts.append(loc) - parts.reverse() - return parts - - def relpath(self): - """ Return this path as a relative path, - based from the current working directory. - """ - cwd = self.__class__(os.getcwd()) - return cwd.relpathto(self) - - def relpathto(self, dest): - """ Return a relative path from self to dest. - - If there is no relative path from self to dest, for example if - they reside on different drives in Windows, then this returns - dest.abspath(). - """ - origin = self.abspath() - dest = self.__class__(dest).abspath() - - orig_list = origin.normcase().splitall() - # Don't normcase dest! We want to preserve the case. - dest_list = dest.splitall() - - if orig_list[0] != os.path.normcase(dest_list[0]): - # Can't get here from there. - return dest - - # Find the location where the two paths start to differ. - i = 0 - for start_seg, dest_seg in zip(orig_list, dest_list): - if start_seg != os.path.normcase(dest_seg): - break - i += 1 - - # Now i is the point where the two paths diverge. - # Need a certain number of "os.pardir"s to work up - # from the origin to the point of divergence. - segments = [os.pardir] * (len(orig_list) - i) - # Need to add the diverging part of dest_list. - segments += dest_list[i:] - if len(segments) == 0: - # If they happen to be identical, use os.curdir. - relpath = os.curdir - else: - relpath = os.path.join(*segments) - return self.__class__(relpath) - - # --- Listing, searching, walking, and matching - - def listdir(self, pattern=None): - """ D.listdir() -> List of items in this directory. - - Use D.files() or D.dirs() instead if you want a listing - of just files or just subdirectories. - - The elements of the list are path objects. - - With the optional 'pattern' argument, this only lists - items whose names match the given pattern. - """ - names = os.listdir(self) - if pattern is not None: - names = fnmatch.filter(names, pattern) - return [self / child for child in names] - - def dirs(self, pattern=None): - """ D.dirs() -> List of this directory's subdirectories. - - The elements of the list are path objects. - This does not walk recursively into subdirectories - (but see path.walkdirs). - - With the optional 'pattern' argument, this only lists - directories whose names match the given pattern. For - example, d.dirs('build-*'). - """ - return [p for p in self.listdir(pattern) if p.isdir()] - - def files(self, pattern=None): - """ D.files() -> List of the files in this directory. - - The elements of the list are path objects. - This does not walk into subdirectories (see path.walkfiles). - - With the optional 'pattern' argument, this only lists files - whose names match the given pattern. For example, - d.files('*.pyc'). - """ - - return [p for p in self.listdir(pattern) if p.isfile()] - - def walk(self, pattern=None, errors='strict'): - """ D.walk() -> iterator over files and subdirs, recursively. - - The iterator yields path objects naming each child item of - this directory and its descendants. This requires that - D.isdir(). - - This performs a depth-first traversal of the directory tree. - Each directory is returned just before all its children. - - The errors= keyword argument controls behavior when an - error occurs. The default is 'strict', which causes an - exception. The other allowed values are 'warn', which - reports the error via warnings.warn(), and 'ignore'. - """ - if errors not in ('strict', 'warn', 'ignore'): - raise ValueError("invalid errors parameter") - - try: - childList = self.listdir() - except Exception: - if errors == 'ignore': - return - elif errors == 'warn': - warnings.warn( - "Unable to list directory '%s': %s" - % (self, sys.exc_info()[1]), - TreeWalkWarning) - return - else: - raise - - for child in childList: - if pattern is None or child.fnmatch(pattern): - yield child - try: - isdir = child.isdir() - except Exception: - if errors == 'ignore': - isdir = False - elif errors == 'warn': - warnings.warn( - "Unable to access '%s': %s" - % (child, sys.exc_info()[1]), - TreeWalkWarning) - isdir = False - else: - raise - - if isdir: - for item in child.walk(pattern, errors): - yield item - - def walkdirs(self, pattern=None, errors='strict'): - """ D.walkdirs() -> iterator over subdirs, recursively. - - With the optional 'pattern' argument, this yields only - directories whose names match the given pattern. For - example, mydir.walkdirs('*test') yields only directories - with names ending in 'test'. - - The errors= keyword argument controls behavior when an - error occurs. The default is 'strict', which causes an - exception. The other allowed values are 'warn', which - reports the error via warnings.warn(), and 'ignore'. - """ - if errors not in ('strict', 'warn', 'ignore'): - raise ValueError("invalid errors parameter") - - try: - dirs = self.dirs() - except Exception: - if errors == 'ignore': - return - elif errors == 'warn': - warnings.warn( - "Unable to list directory '%s': %s" - % (self, sys.exc_info()[1]), - TreeWalkWarning) - return - else: - raise - - for child in dirs: - if pattern is None or child.fnmatch(pattern): - yield child - for subsubdir in child.walkdirs(pattern, errors): - yield subsubdir - - def walkfiles(self, pattern=None, errors='strict'): - """ D.walkfiles() -> iterator over files in D, recursively. - - The optional argument, pattern, limits the results to files - with names that match the pattern. For example, - mydir.walkfiles('*.tmp') yields only files with the .tmp - extension. - """ - if errors not in ('strict', 'warn', 'ignore'): - raise ValueError("invalid errors parameter") - - try: - childList = self.listdir() - except Exception: - if errors == 'ignore': - return - elif errors == 'warn': - warnings.warn( - "Unable to list directory '%s': %s" - % (self, sys.exc_info()[1]), - TreeWalkWarning) - return - else: - raise - - for child in childList: - try: - isfile = child.isfile() - isdir = not isfile and child.isdir() - except: - if errors == 'ignore': - continue - elif errors == 'warn': - warnings.warn( - "Unable to access '%s': %s" - % (self, sys.exc_info()[1]), - TreeWalkWarning) - continue - else: - raise - - if isfile: - if pattern is None or child.fnmatch(pattern): - yield child - elif isdir: - for f in child.walkfiles(pattern, errors): - yield f - - def fnmatch(self, pattern): - """ Return True if self.name matches the given pattern. - - pattern - A filename pattern with wildcards, - for example '*.py'. - """ - return fnmatch.fnmatch(self.name, pattern) - - def glob(self, pattern): - """ Return a list of path objects that match the pattern. - - pattern - a path relative to this directory, with wildcards. - - For example, path('/users').glob('*/bin/*') returns a list - of all the files users have in their bin directories. - """ - cls = self.__class__ - return [cls(s) for s in glob.glob(_base(self / pattern))] - - # - # --- Reading or writing an entire file at once. - - def open(self, mode='r'): - """ Open this file. Return a file object. """ - return open(self, mode) - - def bytes(self): - """ Open this file, read all bytes, return them as a string. """ - f = self.open('rb') - try: - return f.read() - finally: - f.close() - - def write_bytes(self, bytes, append=False): - """ Open this file and write the given bytes to it. - - Default behavior is to overwrite any existing file. - Call p.write_bytes(bytes, append=True) to append instead. - """ - if append: - mode = 'ab' - else: - mode = 'wb' - f = self.open(mode) - try: - f.write(bytes) - finally: - f.close() - - def text(self, encoding=None, errors='strict'): - r""" Open this file, read it in, return the content as a string. - - This uses 'U' mode in Python 2.3 and later, so '\r\n' and '\r' - are automatically translated to '\n'. - - Optional arguments: - - encoding - The Unicode encoding (or character set) of - the file. If present, the content of the file is - decoded and returned as a unicode object; otherwise - it is returned as an 8-bit str. - errors - How to handle Unicode errors; see help(str.decode) - for the options. Default is 'strict'. - """ - if encoding is None: - # 8-bit - f = self.open(_textmode) - try: - return f.read() - finally: - f.close() - else: - # Unicode - f = codecs.open(self, 'r', encoding, errors) - # (Note - Can't use 'U' mode here, since codecs.open - # doesn't support 'U' mode, even in Python 2.3.) - try: - t = f.read() - finally: - f.close() - return (t.replace(u('\r\n'), u('\n')) - .replace(u('\r\x85'), u('\n')) - .replace(u('\r'), u('\n')) - .replace(u('\x85'), u('\n')) - .replace(u('\u2028'), u('\n'))) - - def write_text(self, text, encoding=None, errors='strict', linesep=os.linesep, append=False): - r""" Write the given text to this file. - - The default behavior is to overwrite any existing file; - to append instead, use the 'append=True' keyword argument. - - There are two differences between path.write_text() and - path.write_bytes(): newline handling and Unicode handling. - See below. - - Parameters: - - - text - str/unicode - The text to be written. - - - encoding - str - The Unicode encoding that will be used. - This is ignored if 'text' isn't a Unicode string. - - - errors - str - How to handle Unicode encoding errors. - Default is 'strict'. See help(unicode.encode) for the - options. This is ignored if 'text' isn't a Unicode - string. - - - linesep - keyword argument - str/unicode - The sequence of - characters to be used to mark end-of-line. The default is - os.linesep. You can also specify None; this means to - leave all newlines as they are in 'text'. - - - append - keyword argument - bool - Specifies what to do if - the file already exists (True: append to the end of it; - False: overwrite it.) The default is False. - - - --- Newline handling. - - write_text() converts all standard end-of-line sequences - ('\n', '\r', and '\r\n') to your platform's default end-of-line - sequence (see os.linesep; on Windows, for example, the - end-of-line marker is '\r\n'). - - If you don't like your platform's default, you can override it - using the 'linesep=' keyword argument. If you specifically want - write_text() to preserve the newlines as-is, use 'linesep=None'. - - This applies to Unicode text the same as to 8-bit text, except - there are three additional standard Unicode end-of-line sequences: - u'\x85', u'\r\x85', and u'\u2028'. - - (This is slightly different from when you open a file for - writing with fopen(filename, "w") in C or open(filename, 'w') - in Python.) - - - --- Unicode - - If 'text' isn't Unicode, then apart from newline handling, the - bytes are written verbatim to the file. The 'encoding' and - 'errors' arguments are not used and must be omitted. - - If 'text' is Unicode, it is first converted to bytes using the - specified 'encoding' (or the default encoding if 'encoding' - isn't specified). The 'errors' argument applies only to this - conversion. - - """ - if is_unicode(text): - if linesep is not None: - # Convert all standard end-of-line sequences to - # ordinary newline characters. - text = (text.replace(u('\r\n'), u('\n')) - .replace(u('\r\x85'), u('\n')) - .replace(u('\r'), u('\n')) - .replace(u('\x85'), u('\n')) - .replace(u('\u2028'), u('\n'))) - text = text.replace(u('\n'), linesep) - if encoding is None: - encoding = sys.getdefaultencoding() - bytes = text.encode(encoding, errors) - else: - # It is an error to specify an encoding if 'text' is - # an 8-bit string. - assert encoding is None - - if linesep is not None: - text = (text.replace('\r\n', '\n') - .replace('\r', '\n')) - bytes = text.replace('\n', linesep) - - self.write_bytes(bytes, append) - - def lines(self, encoding=None, errors='strict', retain=True): - r""" Open this file, read all lines, return them in a list. - - Optional arguments: - encoding - The Unicode encoding (or character set) of - the file. The default is None, meaning the content - of the file is read as 8-bit characters and returned - as a list of (non-Unicode) str objects. - errors - How to handle Unicode errors; see help(str.decode) - for the options. Default is 'strict' - retain - If true, retain newline characters; but all newline - character combinations ('\r', '\n', '\r\n') are - translated to '\n'. If false, newline characters are - stripped off. Default is True. - - This uses 'U' mode in Python 2.3 and later. - """ - if encoding is None and retain: - f = self.open(_textmode) - try: - return f.readlines() - finally: - f.close() - else: - return self.text(encoding, errors).splitlines(retain) - - def write_lines(self, lines, encoding=None, errors='strict', - linesep=os.linesep, append=False): - r""" Write the given lines of text to this file. - - By default this overwrites any existing file at this path. - - This puts a platform-specific newline sequence on every line. - See 'linesep' below. - - lines - A list of strings. - - encoding - A Unicode encoding to use. This applies only if - 'lines' contains any Unicode strings. - - errors - How to handle errors in Unicode encoding. This - also applies only to Unicode strings. - - linesep - The desired line-ending. This line-ending is - applied to every line. If a line already has any - standard line ending ('\r', '\n', '\r\n', u'\x85', - u'\r\x85', u'\u2028'), that will be stripped off and - this will be used instead. The default is os.linesep, - which is platform-dependent ('\r\n' on Windows, '\n' on - Unix, etc.) Specify None to write the lines as-is, - like file.writelines(). - - Use the keyword argument append=True to append lines to the - file. The default is to overwrite the file. Warning: - When you use this with Unicode data, if the encoding of the - existing data in the file is different from the encoding - you specify with the encoding= parameter, the result is - mixed-encoding data, which can really confuse someone trying - to read the file later. - """ - if append: - mode = 'ab' - else: - mode = 'wb' - f = self.open(mode) - try: - for line in lines: - isUnicode = is_unicode(line) - if linesep is not None: - # Strip off any existing line-end and add the - # specified linesep string. - if isUnicode: - if line[-2:] in (u('\r\n'), u('\x0d\x85')): - line = line[:-2] - elif line[-1:] in (u('\r'), u('\n'), - u('\x85'), u('\u2028')): - line = line[:-1] - else: - if line[-2:] == '\r\n': - line = line[:-2] - elif line[-1:] in ('\r', '\n'): - line = line[:-1] - line += linesep - if isUnicode: - if encoding is None: - encoding = sys.getdefaultencoding() - line = line.encode(encoding, errors) - f.write(line) - finally: - f.close() - - def read_md5(self): - """ Calculate the md5 hash for this file. - - This reads through the entire file. - """ - return self.read_hash('md5') - - def _hash(self, hash_name): - f = self.open('rb') - try: - m = hashlib.new(hash_name) - while True: - d = f.read(8192) - if not d: - break - m.update(d) - return m - finally: - f.close() - - def read_hash(self, hash_name): - """ Calculate given hash for this file. - - List of supported hashes can be obtained from hashlib package. This - reads the entire file. - """ - return self._hash(hash_name).digest() - - def read_hexhash(self, hash_name): - """ Calculate given hash for this file, returning hexdigest. - - List of supported hashes can be obtained from hashlib package. This - reads the entire file. - """ - return self._hash(hash_name).hexdigest() - - # --- Methods for querying the filesystem. - # N.B. On some platforms, the os.path functions may be implemented in C - # (e.g. isdir on Windows, Python 3.2.2), and compiled functions don't get - # bound. Playing it safe and wrapping them all in method calls. - - def isabs(self): return os.path.isabs(self) - def exists(self): return os.path.exists(self) - def isdir(self): return os.path.isdir(self) - def isfile(self): return os.path.isfile(self) - def islink(self): return os.path.islink(self) - def ismount(self): return os.path.ismount(self) - - if hasattr(os.path, 'samefile'): - def samefile(self): return os.path.samefile(self) - - def getatime(self): return os.path.getatime(self) - atime = property( - getatime, None, None, - """ Last access time of the file. """) - - def getmtime(self): return os.path.getmtime(self) - mtime = property( - getmtime, None, None, - """ Last-modified time of the file. """) - - if hasattr(os.path, 'getctime'): - def getctime(self): return os.path.getctime(self) - ctime = property( - getctime, None, None, - """ Creation time of the file. """) - - def getsize(self): return os.path.getsize(self) - size = property( - getsize, None, None, - """ Size of the file, in bytes. """) - - if hasattr(os, 'access'): - def access(self, mode): - """ Return true if current user has access to this path. - - mode - One of the constants os.F_OK, os.R_OK, os.W_OK, os.X_OK - """ - return os.access(self, mode) - - def stat(self): - """ Perform a stat() system call on this path. """ - return os.stat(self) - - def lstat(self): - """ Like path.stat(), but do not follow symbolic links. """ - return os.lstat(self) - - def get_owner(self): - r""" Return the name of the owner of this file or directory. - - This follows symbolic links. - - On Windows, this returns a name of the form ur'DOMAIN\User Name'. - On Windows, a group can own a file or directory. - """ - if os.name == 'nt': - if win32security is None: - raise Exception("path.owner requires win32all to be installed") - desc = win32security.GetFileSecurity( - self, win32security.OWNER_SECURITY_INFORMATION) - sid = desc.GetSecurityDescriptorOwner() - account, domain, typecode = win32security.LookupAccountSid(None, sid) - return domain + u('\\') + account - else: - if pwd is None: - raise NotImplementedError("path.owner is not implemented on this platform.") - st = self.stat() - return pwd.getpwuid(st.st_uid).pw_name - - owner = property( - get_owner, None, None, - """ Name of the owner of this file or directory. """) - - if hasattr(os, 'statvfs'): - def statvfs(self): - """ Perform a statvfs() system call on this path. """ - return os.statvfs(self) - - if hasattr(os, 'pathconf'): - def pathconf(self, name): - return os.pathconf(self, name) - - # - # --- Modifying operations on files and directories - - def utime(self, times): - """ Set the access and modified times of this file. """ - os.utime(self, times) - - def chmod(self, mode): - os.chmod(self, mode) - - if hasattr(os, 'chown'): - def chown(self, uid, gid): - os.chown(self, uid, gid) - - def rename(self, new): - os.rename(self, new) - - def renames(self, new): - os.renames(self, new) - - # - # --- Create/delete operations on directories - - def mkdir(self, mode=MODE_0777): - os.mkdir(self, mode) - - def mkdir_p(self, mode=MODE_0777): - try: - self.mkdir(mode) - except OSError as e: - if e.errno != errno.EEXIST: - raise - - def makedirs(self, mode=MODE_0777): - os.makedirs(self, mode) - - def makedirs_p(self, mode=MODE_0777): - try: - self.makedirs(mode) - except OSError as e: - if e.errno != errno.EEXIST: - raise - - def rmdir(self): - os.rmdir(self) - - def rmdir_p(self): - try: - self.rmdir() - except OSError as e: - if e.errno != errno.ENOTEMPTY and e.errno != errno.EEXIST: - raise - - def removedirs(self): - os.removedirs(self) - - def removedirs_p(self): - try: - self.removedirs() - except OSError as e: - if e.errno != errno.ENOTEMPTY and e.errno != errno.EEXIST: - raise - - # --- Modifying operations on files - - def touch(self): - """ Set the access/modified times of this file to the current time. - Create the file if it does not exist. - """ - fd = os.open(self, os.O_WRONLY | os.O_CREAT, MODE_0666) - os.close(fd) - os.utime(self, None) - - def remove(self): - os.remove(self) - - def remove_p(self): - try: - self.unlink() - except OSError as e: - if e.errno != errno.ENOENT: - raise - - def unlink(self): - os.unlink(self) - - def unlink_p(self): - self.remove_p() - - # --- Links - - if hasattr(os, 'link'): - def link(self, newpath): - """ Create a hard link at 'newpath', pointing to this file. """ - os.link(self, newpath) - - if hasattr(os, 'symlink'): - def symlink(self, newlink): - """ Create a symbolic link at 'newlink', pointing here. """ - os.symlink(self, newlink) - - if hasattr(os, 'readlink'): - def readlink(self): - """ Return the path to which this symbolic link points. - - The result may be an absolute or a relative path. - """ - return self.__class__(os.readlink(self)) - - def readlinkabs(self): - """ Return the path to which this symbolic link points. - - The result is always an absolute path. - """ - p = self.readlink() - if p.isabs(): - return p - else: - return (self.parent / p).abspath() - - # - # --- High-level functions from shutil - - copyfile = shutil.copyfile - copymode = shutil.copymode - copystat = shutil.copystat - copy = shutil.copy - copy2 = shutil.copy2 - copytree = shutil.copytree - if hasattr(shutil, 'move'): - move = shutil.move - rmtree = shutil.rmtree - - def rmtree_p(self): - try: - self.rmtree() - except OSError as e: - if e.errno != errno.ENOENT: - raise - - # - # --- Special stuff from os - - if hasattr(os, 'chroot'): - def chroot(self): - os.chroot(self) - - if hasattr(os, 'startfile'): - def startfile(self): - os.startfile(self) diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/utils/external/pickleshare.py spyder-3.0.2+dfsg1/spyder/utils/external/pickleshare.py --- spyder-2.3.8+dfsg1/spyder/utils/external/pickleshare.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/utils/external/pickleshare.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,370 +0,0 @@ -#!/usr/bin/env python - -""" PickleShare - a small 'shelve' like datastore with concurrency support - -Like shelve, a PickleShareDB object acts like a normal dictionary. Unlike -shelve, many processes can access the database simultaneously. Changing a -value in database is immediately visible to other processes accessing the -same database. - -Concurrency is possible because the values are stored in separate files. Hence -the "database" is a directory where *all* files are governed by PickleShare. - -Example usage:: - - from pickleshare import * - db = PickleShareDB('~/testpickleshare') - db.clear() - print "Should be empty:",db.items() - db['hello'] = 15 - db['aku ankka'] = [1,2,313] - db['paths/are/ok/key'] = [1,(5,46)] - print db.keys() - del db['aku ankka'] - -This module is certainly not ZODB, but can be used for low-load -(non-mission-critical) situations where tiny code size trumps the -advanced features of a "real" object database. - -Installation guide: easy_install pickleshare - -Author: Ville Vainio -License: MIT open source license. - -""" - -from __future__ import print_function - -from spyderlib.utils.external.path import path as Path -from spyderlib.py3compat import pickle, MutableMapping - -import os -import stat -import time -import glob -import errno - -def gethashfile(key): - return ("%02x" % abs(hash(key) % 256))[-2:] - -_sentinel = object() - -class PickleShareDB(MutableMapping): - """ The main 'connection' object for PickleShare database """ - def __init__(self, root): - """ Return a db object that will manage the specied directory""" - self.root = Path(root).expanduser().abspath() - if not self.root.isdir(): - self.root.makedirs() - # cache has { 'key' : (obj, orig_mod_time) } - self.cache = {} - - #========================================================================== - # Only affects Python 3 - def __iter__(self): - return iter(self.cache) - def __len__(self): - return len(self.cache) - #========================================================================== - - def __getitem__(self, key): - """ db['key'] reading """ - fil = self.root / key - try: - mtime = (fil.stat()[stat.ST_MTIME]) - except OSError: - raise KeyError(key) - - if fil in self.cache and mtime == self.cache[fil][1]: - return self.cache[fil][0] - try: - # The cached item has expired, need to read - obj = pickle.load(fil.open('rb')) - except: - raise KeyError(key) - - self.cache[fil] = (obj, mtime) - return obj - - def __setitem__(self, key, value): - """ db['key'] = 5 """ - fil = self.root / key - parent = fil.parent - if parent and not parent.isdir(): - parent.makedirs() - # We specify protocol 2, so that we can mostly go between Python 2 - # and Python 3. We can upgrade to protocol 3 when Python 2 is obsolete. - pickled = pickle.dump(value, fil.open('wb'), protocol=2) - try: - self.cache[fil] = (value, fil.mtime) - except OSError as e: - if e.errno != errno.ENOENT: - raise - - def hset(self, hashroot, key, value): - """ hashed set """ - hroot = self.root / hashroot - if not hroot.isdir(): - hroot.makedirs() - hfile = hroot / gethashfile(key) - d = self.get(hfile, {}) - d.update( {key : value}) - self[hfile] = d - - - - def hget(self, hashroot, key, default = _sentinel, fast_only = True): - """ hashed get """ - hroot = self.root / hashroot - hfile = hroot / gethashfile(key) - - d = self.get(hfile, _sentinel ) - #print "got dict",d,"from",hfile - if d is _sentinel: - if fast_only: - if default is _sentinel: - raise KeyError(key) - - return default - - # slow mode ok, works even after hcompress() - d = self.hdict(hashroot) - - return d.get(key, default) - - def hdict(self, hashroot): - """ Get all data contained in hashed category 'hashroot' as dict """ - hfiles = self.keys(hashroot + "/*") - hfiles.sort() - last = len(hfiles) and hfiles[-1] or '' - if last.endswith('xx'): - # print "using xx" - hfiles = [last] + hfiles[:-1] - - all = {} - - for f in hfiles: - # print "using",f - try: - all.update(self[f]) - except KeyError: - print("Corrupt", f, "deleted - hset is not threadsafe!") - del self[f] - - self.uncache(f) - - return all - - def hcompress(self, hashroot): - """ Compress category 'hashroot', so hset is fast again - - hget will fail if fast_only is True for compressed items (that were - hset before hcompress). - - """ - hfiles = self.keys(hashroot + "/*") - all = {} - for f in hfiles: - # print "using",f - all.update(self[f]) - self.uncache(f) - - self[hashroot + '/xx'] = all - for f in hfiles: - p = self.root / f - if p.basename() == 'xx': - continue - p.remove() - - - - def __delitem__(self, key): - """ del db["key"] """ - fil = self.root / key - self.cache.pop(fil, None) - try: - fil.remove() - except OSError: - # notfound and permission denied are ok - we - # lost, the other process wins the conflict - pass - - def _normalized(self, p): - """ Make a key suitable for user's eyes """ - return str(self.root.relpathto(p)).replace('\\', '/') - - def keys(self, globpat = None): - """ All keys in DB, or all keys matching a glob""" - - if globpat is None: - files = self.root.walkfiles() - else: - files = [Path(p) for p in glob.glob(self.root/globpat)] - return [self._normalized(p) for p in files if p.isfile()] - - def uncache(self,*items): - """ Removes all, or specified items from cache - - Use this after reading a large amount of large objects - to free up memory, when you won't be needing the objects - for a while. - - """ - if not items: - self.cache = {} - for it in items: - self.cache.pop(it, None) - - def waitget(self,key, maxwaittime = 60 ): - """ Wait (poll) for a key to get a value - - Will wait for `maxwaittime` seconds before raising a KeyError. - The call exits normally if the `key` field in db gets a value - within the timeout period. - - Use this for synchronizing different processes or for ensuring - that an unfortunately timed "db['key'] = newvalue" operation - in another process (which causes all 'get' operation to cause a - KeyError for the duration of pickling) won't screw up your program - logic. - """ - - wtimes = [0.2] * 3 + [0.5] * 2 + [1] - tries = 0 - waited = 0 - while 1: - try: - val = self[key] - return val - except KeyError: - pass - - if waited > maxwaittime: - raise KeyError(key) - - time.sleep(wtimes[tries]) - waited+=wtimes[tries] - if tries < len(wtimes) -1: - tries+=1 - - def getlink(self, folder): - """ Get a convenient link for accessing items """ - return PickleShareLink(self, folder) - - def __repr__(self): - return "PickleShareDB('%s')" % self.root - - - -class PickleShareLink: - """ A shortdand for accessing nested PickleShare data conveniently. - - Created through PickleShareDB.getlink(), example:: - - lnk = db.getlink('myobjects/test') - lnk.foo = 2 - lnk.bar = lnk.foo + 5 - - """ - def __init__(self, db, keydir ): - self.__dict__.update(locals()) - - def __getattr__(self, key): - return self.__dict__['db'][self.__dict__['keydir']+'/' + key] - def __setattr__(self, key, val): - self.db[self.keydir+'/' + key] = val - def __repr__(self): - db = self.__dict__['db'] - keys = db.keys( self.__dict__['keydir'] +"/*") - return "" % ( - self.__dict__['keydir'], - ";".join([Path(k).basename() for k in keys])) - - -def test(): - db = PickleShareDB('~/testpickleshare') - db.clear() - print("Should be empty:", list(db.items())) - db['hello'] = 15 - db['aku ankka'] = [1, 2, 313] - db['paths/nest/ok/keyname'] = [1, (5, 46)] - db.hset('hash', 'aku', 12) - db.hset('hash', 'ankka', 313) - print("12 =", db.hget('hash', 'aku')) - print("313 =", db.hget('hash', 'ankka')) - print("all hashed", db.hdict('hash')) - print(list(db.keys())) - print(db.keys('paths/nest/ok/k*')) - print(dict(db)) # snapsot of whole db - db.uncache() # frees memory, causes re-reads later - - # shorthand for accessing deeply nested files - lnk = db.getlink('myobjects/test') - lnk.foo = 2 - lnk.bar = lnk.foo + 5 - print(lnk.bar) # 7 - -def stress(): - db = PickleShareDB('~/fsdbtest') - import time, sys - for i in range(1000): - for j in range(1000): - if i % 15 == 0 and i < 200: - if str(j) in db: - del db[str(j)] - continue - - if j%33 == 0: - time.sleep(0.02) - - db[str(j)] = db.get(str(j), []) + [(i, j, "proc %d" % os.getpid())] - db.hset('hash', j, db.hget('hash', j, 15) + 1 ) - - print(i, end=' ') - sys.stdout.flush() - if i % 10 == 0: - db.uncache() - -def main(): - import textwrap - usage = textwrap.dedent("""\ - pickleshare - manage PickleShare databases - - Usage: - - pickleshare dump /path/to/db > dump.txt - pickleshare load /path/to/db < dump.txt - pickleshare test /path/to/db - """) - DB = PickleShareDB - import sys - if len(sys.argv) < 2: - print(usage) - return - - cmd = sys.argv[1] - args = sys.argv[2:] - if cmd == 'dump': - if not args: args= ['.'] - db = DB(args[0]) - import pprint - pprint.pprint(list(db.items())) - elif cmd == 'load': - cont = sys.stdin.read() - db = DB(args[0]) - data = eval(cont) - db.clear() - for k, v in list(db.items()): - db[k] = v - elif cmd == 'testwait': - db = DB(args[0]) - db.clear() - print(db.waitget('250')) - elif cmd == 'test': - test() - stress() - -if __name__== "__main__": - main() - - diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/utils/fixtures.py spyder-3.0.2+dfsg1/spyder/utils/fixtures.py --- spyder-2.3.8+dfsg1/spyder/utils/fixtures.py 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/utils/fixtures.py 2016-10-25 02:05:23.000000000 +0200 @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# +# Copyright © Spyder Project Contributors +# Licensed under the terms of the MIT License +# + +""" +Testing utilities to be used with pytest. +""" + +# Standard library imports +import shutil +import tempfile + +# Third party imports +import pytest + +# Local imports +from spyder.config.user import UserConfig +from spyder.config.main import CONF_VERSION, DEFAULTS + + +@pytest.fixture +def tmpconfig(request): + """ + Fixtures that returns a temporary CONF element. + """ + SUBFOLDER = tempfile.mkdtemp() + CONF = UserConfig('spyder-test', + defaults=DEFAULTS, + version=CONF_VERSION, + subfolder=SUBFOLDER, + raw_mode=True, + ) + + def fin(): + """ + Fixture finalizer to delete the temporary CONF element. + """ + shutil.rmtree(SUBFOLDER) + + request.addfinalizer(fin) + return CONF diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/utils/help/conf.py spyder-3.0.2+dfsg1/spyder/utils/help/conf.py --- spyder-2.3.8+dfsg1/spyder/utils/help/conf.py 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/utils/help/conf.py 2016-10-25 02:05:23.000000000 +0200 @@ -0,0 +1,116 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2009 Tim Dumol +# Copyright (C) Spyder Project Contributors +# Distributed under the terms of the BSD License + +"""Sphinx conf file for the Help plugin rich text mode""" + +# 3rd party imports +from sphinx import __version__ as sphinx_version + +# Local imports +from spyder.config.main import CONF +from spyder.py3compat import u + +#============================================================================== +# General configuration +#============================================================================== + +# If your extensions are in another directory, add it here. If the directory +# is relative to the documentation root, use os.path.abspath to make it +# absolute, like shown here. +#sys.path.append(os.path.abspath('.')) + +# Add any Sphinx extension module names here, as strings. They can be extensions +# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. + +# We need jsmath to get pretty plain-text latex in docstrings +math = CONF.get('help', 'math', '') + +if sphinx_version < "1.1" or not math: + extensions = ['sphinx.ext.jsmath'] +else: + extensions = ['sphinx.ext.mathjax'] + +# For scipy and matplotlib docstrings, which need this extension to +# be rendered correctly (see Issue 1138) +extensions.append('sphinx.ext.autosummary') + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['templates'] + +# MathJax load path (doesn't have effect for sphinx 1.0-) +mathjax_path = 'MathJax/MathJax.js' + +# JsMath load path (doesn't have effect for sphinx 1.1+) +jsmath_path = 'easy/load.js' + +# The suffix of source filenames. +source_suffix = '.rst' + +# The master toctree document. +master_doc = 'docstring' + +# General information about the project. +project = u("Spyder Help plugin") +copyright = u('The Spyder Project Contributors') + +# List of directories, relative to source directory, that shouldn't be searched +# for source files. +exclude_trees = ['.build'] + +# The reST default role (used for this markup: `text`) to use for all documents. +# +# TODO: This role has to be set on a per project basis, i.e. numpy, sympy, +# mpmath, etc, use different default_role's which give different rendered +# docstrings. Setting this to None until it's solved. +default_role = 'None' + +# If true, '()' will be appended to :func: etc. cross-reference text. +add_function_parentheses = True + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +#============================================================================== +# Options for HTML output +#============================================================================== + +# The style sheet to use for HTML and HTML Help pages. A file of that name +# must exist either in Sphinx' static/ path, or in one of the custom paths +# given in html_static_path. +html_style = 'default.css' + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +html_short_title = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['static'] + +# A dictionary of values to pass into the template engine’s context for all +# pages +html_context = {} + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# If false, no module index is generated. +html_use_modindex = False + +# If false, no index is generated. +html_use_index = False + +# If true, the index is split into individual pages for each letter. +html_split_index = False + +# If true, the reST sources are included in the HTML build as _sources/. +html_copy_source = False + diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/utils/help/__init__.py spyder-3.0.2+dfsg1/spyder/utils/help/__init__.py --- spyder-2.3.8+dfsg1/spyder/utils/help/__init__.py 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/utils/help/__init__.py 2016-10-25 02:05:23.000000000 +0200 @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# +# Copyright © Spyder Project Contributors +# Licensed under the terms of the MIT or BSD Licenses +# (See every file for its license) + +""" +spyder.utils.help +================= + +Configuration files for the Help plugin rich text mode +""" + +import sys +from spyder.config.base import get_module_source_path +sys.path.insert(0, get_module_source_path(__name__)) diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/utils/help/js/collapse_sections.js spyder-3.0.2+dfsg1/spyder/utils/help/js/collapse_sections.js --- spyder-2.3.8+dfsg1/spyder/utils/help/js/collapse_sections.js 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/utils/help/js/collapse_sections.js 2016-10-25 02:05:23.000000000 +0200 @@ -0,0 +1,65 @@ +//---------------------------------------------------------------------------- +// Toggleable sections +// +// Added expand/collapse functionality to RST sections. +// Code from the Cloud Sphinx theme +// +// Copyright 2011-2012 by Assurance Technologies +// +// Distributed under the terms of the BSD License +//---------------------------------------------------------------------------- + +//============================================================================ +// On document ready +//============================================================================ + +$(document).ready(function (){ + function init(){ + // get header & section, and add static classes + var header = $(this); + var section = header.parent(); + header.addClass("html-toggle-button"); + + // helper to test if url hash is within this section + function contains_hash(){ + var hash = document.location.hash; + return hash && (section[0].id == hash.substr(1) || + section.find(hash.replace(/\./g,"\\.")).length>0); + } + + // helper to control toggle state + function set_state(expanded){ + if(expanded){ + section.addClass("expanded").removeClass("collapsed"); + section.children().show(); + }else{ + section.addClass("collapsed").removeClass("expanded"); + section.children().hide(); + section.children("span:first-child:empty").show(); /* for :ref: span tag */ + header.show(); + } + } + + // initialize state + set_state(section.hasClass("expanded") || contains_hash()); + + // bind toggle callback + header.click(function (){ + section.children().next().slideToggle(300); + set_state(!section.hasClass("expanded")); + $(window).trigger('cloud-section-toggled', section[0]); + }); + + // open section if user jumps to it from w/in page + $(window).bind("hashchange", function () { + if(contains_hash()) { + var link = document.location.hash; + $(link).parents().each(set_state, [true]); + set_state(true); + $('html, body').animate({ scrollTop: $(link).offset().top }, 'fast'); + } + }); + } + + $(".section > h2, .section > h3, .section > h4").each(init); +}); diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/utils/help/js/copy_button.js spyder-3.0.2+dfsg1/spyder/utils/help/js/copy_button.js --- spyder-2.3.8+dfsg1/spyder/utils/help/js/copy_button.js 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/utils/help/js/copy_button.js 2016-10-25 02:05:23.000000000 +0200 @@ -0,0 +1,68 @@ +//---------------------------------------------------------------------------- +// Copy Button +// +// Add a [>>>] button on the top-right corner of code samples to hide +// the >>> and ... prompts and the output and thus make the code +// copyable. +// +// Taken from http://docs.python.org/_static/copybutton.js +//---------------------------------------------------------------------------- + +//============================================================================ +// On document ready +//============================================================================ + +$(document).ready(function() { + var div = $('.highlight-python .highlight,' + + '.highlight-python3 .highlight') + var pre = div.find('pre'); + + // get the styles from the current theme + pre.parent().parent().css('position', 'relative'); + var hide_text = 'Hide the prompts and output'; + var show_text = 'Show the prompts and output'; + var border_width = pre.css('border-top-width'); + var border_style = pre.css('border-top-style'); + var border_color = pre.css('border-top-color'); + var button_styles = { + 'cursor':'pointer', 'position': 'absolute', 'top': '0', 'right': '0', + 'border-color': border_color, 'border-style': border_style, + 'border-width': border_width, 'color': border_color, 'text-size': '75%', + 'font-family': 'monospace', 'padding-left': '0.2em', 'padding-right': '0.2em', + 'margin-right': '10px', 'border-top-right-radius': '4px' + } + + // create and add the button to all the code blocks that contain >>> + div.each(function(index) { + var jthis = $(this); + if (jthis.find('.gp').length > 0) { + var button = $('>>>'); + button.css(button_styles) + button.attr('title', hide_text); + jthis.prepend(button); + } + // tracebacks (.gt) contain bare text elements that need to be + // wrapped in a span to work with .nextUntil() (see later) + jthis.find('pre:has(.gt)').contents().filter(function() { + return ((this.nodeType == 3) && (this.data.trim().length > 0)); + }).wrap(''); + }); + + // define the behavior of the button when it's clicked + $('.copybutton').toggle( + function() { + var button = $(this); + button.parent().find('.go, .gp, .gt').hide(); + button.next('pre').find('.gt').nextUntil('.gp, .go').css('visibility', 'hidden'); + button.css('text-decoration', 'line-through'); + button.attr('title', show_text); + }, + function() { + var button = $(this); + button.parent().find('.go, .gp, .gt').show(); + button.next('pre').find('.gt').nextUntil('.gp, .go').css('visibility', 'visible'); + button.css('text-decoration', 'none'); + button.attr('title', hide_text); + }); +}); + diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/utils/help/js/fix_image_paths.js spyder-3.0.2+dfsg1/spyder/utils/help/js/fix_image_paths.js --- spyder-2.3.8+dfsg1/spyder/utils/help/js/fix_image_paths.js 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/utils/help/js/fix_image_paths.js 2016-10-25 02:05:23.000000000 +0200 @@ -0,0 +1,18 @@ +//---------------------------------------------------------------------------- +// Set absolute path for images +// +// Copyright (c) Spyder Project Contributors +// +// Distributed under the terms of the MIT License +//---------------------------------------------------------------------------- + +//============================================================================ +// On document ready +//============================================================================ +$(document).ready(function () { + $('img').attr('src', function(index, attr){ + var path = attr.split('/') + var img_name = path.reverse()[0] + return 'file:///{{img_path}}' + '/' + img_name + }); +}); diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/utils/help/js/math_config.js spyder-3.0.2+dfsg1/spyder/utils/help/js/math_config.js --- spyder-2.3.8+dfsg1/spyder/utils/help/js/math_config.js 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/utils/help/js/math_config.js 2016-10-25 02:05:23.000000000 +0200 @@ -0,0 +1,74 @@ +//---------------------------------------------------------------------------- +// Math configuration options and hacks +// +// Copyright (C) Spyder Project Contributors +// +// Distributed under the terms of the MIT License. +//---------------------------------------------------------------------------- + +//============================================================================ +// On document ready +//============================================================================ + +{% if right_sphinx_version and math_on %} + +$(document).ready(function () { + + // MathJax config + // -------------- + MathJax.Hub.Config({ + // We are using SVG instead of HTML-CSS because the last one gives + // troubles on QtWebkit. See this thread: + // https://groups.google.com/forum/?fromgroups#!topic/mathjax-users/HKA2lNqv-OQ + jax: ["input/TeX", "output/SVG"], + + // Menu options are not working. It would be useful to have 'Show TeX + // commands', but it opens an external browser pointing to css_path. + // I don't know why that's happening + showMathMenu: false, + messageStyle: "none", + "SVG": { + blacker: 1 + }, + + {% if platform == 'win32' %} + // Change math preview size so that it doesn't look too big while + // redendered + styles: { + ".MathJax_Preview": { + color: "#888", + "font-size": "55%" + } + } + {% endif %} + }); + + // MathJax Hooks + // ------------- + // Put here any code that needs to be evaluated after MathJax has been + // fully loaded + MathJax.Hub.Register.StartupHook("End", function () { + // Eliminate unnecessary margin-bottom for inline math + $('span.math svg').css('margin-bottom', '0px'); + }); + + {% if platform == 'win32' %} + // Windows fix + // ----------- + // Increase font size of math elements because they appear too small + // compared to the surrounding text. + // Use this hack because MathJax 'scale' option seems to not be working + // for SVG. + $('.math').css("color", "transparent"); + $('.math').css("fontSize", "213%"); + {% endif %} +}); + +{% else %} + +$(document).ready(function () { + // Show math in monospace + $('.math').css('font-family', 'monospace'); +}); + +{% endif %} diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/utils/help/js/move_outline.js spyder-3.0.2+dfsg1/spyder/utils/help/js/move_outline.js --- spyder-2.3.8+dfsg1/spyder/utils/help/js/move_outline.js 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/utils/help/js/move_outline.js 2016-10-25 02:05:23.000000000 +0200 @@ -0,0 +1,16 @@ +//---------------------------------------------------------------------------- +// Move Outline section to be the first one +// +// Copyright (c) Spyder Project Contributors +// +// Distributed under the terms of the MIT License +//---------------------------------------------------------------------------- + +//============================================================================ +// On document ready +//============================================================================ + +$(document).ready(function (){ + var first_section_id = $(".section")[0].id; + $("#outline").insertBefore("#" + first_section_id); +}); diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/utils/help/js/utils.js spyder-3.0.2+dfsg1/spyder/utils/help/js/utils.js --- spyder-2.3.8+dfsg1/spyder/utils/help/js/utils.js 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/utils/help/js/utils.js 2016-10-25 02:05:23.000000000 +0200 @@ -0,0 +1,34 @@ +//---------------------------------------------------------------------------- +// Several utility functions to modify docstring webpages while they are +// rendered +// +// Copyright (C) Spyder Project Contributors +// +// Distributed under the terms of the MIT License. +//---------------------------------------------------------------------------- + +//============================================================================ +// On document ready +//============================================================================ + +$(document).ready(function () { + // Remove anchor header links. + // They're used by Sphinx to create crossrefs, so we don't need them + $('a.headerlink').remove(); + + // If the first child in the docstring div is a section, change its class + // to title. This means that the docstring has a real title and we need + // to use it. + // This is really useful to show module docstrings. + var first_doc_child = $('div.docstring').children(':first-child'); + if( first_doc_child.is('div.section') && $('div.title').length == 0 ) { + first_doc_child.removeClass('section').addClass('title'); + }; + + // Change docstring headers from h1 to h2 + // It can only be an h1 and that's the page title + // Taken from http://forum.jquery.com/topic/how-to-replace-h1-h2 + $('div.docstring').find('div.section h1').replaceWith(function () { + return '

    ' + $(this).text() + '

    '; + }); +}); diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/utils/help/sphinxify.py spyder-3.0.2+dfsg1/spyder/utils/help/sphinxify.py --- spyder-2.3.8+dfsg1/spyder/utils/help/sphinxify.py 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/utils/help/sphinxify.py 2016-10-25 02:05:23.000000000 +0200 @@ -0,0 +1,261 @@ +# -*- coding: utf-8 -* + +""" +Process docstrings with Sphinx + +AUTHORS: +- Tim Joseph Dumol (2009-09-29): initial version +- The Spyder Project Contributors: Several changes to make it work with Spyder + +Copyright (C) 2009 Tim Dumol +Copyright (C) Spyder Project Contributors +Distributed under the terms of the BSD License + +Taken from the Sage project (www.sagemath.org). +See here for the original version: +www.sagemath.org/doc/reference/sagenb/misc/sphinxify.html +""" + +# Stdlib imports +import codecs +import os +import os.path as osp +import shutil +import sys +from tempfile import mkdtemp +from xml.sax.saxutils import escape + +# 3rd party imports +from docutils.utils import SystemMessage as SystemMessage +from jinja2 import Environment, FileSystemLoader +import sphinx +from sphinx.application import Sphinx + +# Local imports +from spyder.config.base import (_, get_module_data_path, + get_module_source_path) +from spyder.utils import encoding + + +#----------------------------------------------------------------------------- +# Globals and constants +#----------------------------------------------------------------------------- + +# Note: we do not use __file__ because it won't be working in the stand-alone +# version of Spyder (i.e. the py2exe or cx_Freeze build) +CONFDIR_PATH = get_module_source_path('spyder.utils.help') +CSS_PATH = osp.join(CONFDIR_PATH, 'static', 'css') +JS_PATH = osp.join(CONFDIR_PATH, 'js') + +# To let Debian packagers redefine the MathJax and JQuery locations so they can +# use their own packages for them. See Issue 1230, comment #7. +MATHJAX_PATH = get_module_data_path('spyder', + relpath=osp.join('utils', 'help', + JS_PATH, 'mathjax'), + attr_name='MATHJAXPATH') + +JQUERY_PATH = get_module_data_path('spyder', + relpath=osp.join('utils', 'help', + JS_PATH), + attr_name='JQUERYPATH') + +#----------------------------------------------------------------------------- +# Utility functions +#----------------------------------------------------------------------------- + +def is_sphinx_markup(docstring): + """Returns whether a string contains Sphinx-style ReST markup.""" + # this could be made much more clever + return ("`" in docstring or "::" in docstring) + + +def warning(message): + """Print a warning message on the rich text view""" + env = Environment() + env.loader = FileSystemLoader(osp.join(CONFDIR_PATH, 'templates')) + warning = env.get_template("warning.html") + return warning.render(css_path=CSS_PATH, text=message) + + +def usage(title, message, tutorial_message, tutorial): + """Print a usage message on the rich text view""" + env = Environment() + env.loader = FileSystemLoader(osp.join(CONFDIR_PATH, 'templates')) + usage = env.get_template("usage.html") + return usage.render(css_path=CSS_PATH, title=title, intro_message=message, + tutorial_message=tutorial_message, tutorial=tutorial) + + +def generate_context(name='', argspec='', note='', math=False, collapse=False, + img_path=''): + """ + Generate the html_context dictionary for our Sphinx conf file. + + This is a set of variables to be passed to the Jinja template engine and + that are used to control how the webpage is rendered in connection with + Sphinx + + Parameters + ---------- + name : str + Object's name. + note : str + A note describing what type has the function or method being + introspected + argspec : str + Argspec of the the function or method being introspected + math : bool + Turn on/off Latex rendering on the OI. If False, Latex will be shown in + plain text. + collapse : bool + Collapse sections + img_path : str + Path for images relative to the file containing the docstring + + Returns + ------- + A dict of strings to be used by Jinja to generate the webpage + """ + + if img_path and os.name == 'nt': + img_path = img_path.replace('\\', '/') + + context = \ + { + # Arg dependent variables + 'math_on': 'true' if math else '', + 'name': name, + 'argspec': argspec, + 'note': note, + 'collapse': collapse, + 'img_path': img_path, + + # Static variables + 'css_path': CSS_PATH, + 'js_path': JS_PATH, + 'jquery_path': JQUERY_PATH, + 'mathjax_path': MATHJAX_PATH, + 'right_sphinx_version': '' if sphinx.__version__ < "1.1" else 'true', + 'platform': sys.platform + } + + return context + + +def sphinxify(docstring, context, buildername='html'): + """ + Runs Sphinx on a docstring and outputs the processed documentation. + + Parameters + ---------- + docstring : str + a ReST-formatted docstring + + context : dict + Variables to be passed to the layout template to control how its + rendered (through the Sphinx variable *html_context*). + + buildername: str + It can be either `html` or `text`. + + Returns + ------- + An Sphinx-processed string, in either HTML or plain text format, depending + on the value of `buildername` + """ + + srcdir = mkdtemp() + srcdir = encoding.to_unicode_from_fs(srcdir) + + base_name = osp.join(srcdir, 'docstring') + rst_name = base_name + '.rst' + + if buildername == 'html': + suffix = '.html' + else: + suffix = '.txt' + output_name = base_name + suffix + + # This is needed so users can type \\ on latex eqnarray envs inside raw + # docstrings + if context['right_sphinx_version'] and context['math_on']: + docstring = docstring.replace('\\\\', '\\\\\\\\') + + # Add a class to several characters on the argspec. This way we can + # highlight them using css, in a similar way to what IPython does. + # NOTE: Before doing this, we escape common html chars so that they + # don't interfere with the rest of html present in the page + argspec = escape(context['argspec']) + for char in ['=', ',', '(', ')', '*', '**']: + argspec = argspec.replace(char, + '' + char + '') + context['argspec'] = argspec + + doc_file = codecs.open(rst_name, 'w', encoding='utf-8') + doc_file.write(docstring) + doc_file.close() + + temp_confdir = False + if temp_confdir: + # TODO: This may be inefficient. Find a faster way to do it. + confdir = mkdtemp() + confdir = encoding.to_unicode_from_fs(confdir) + generate_configuration(confdir) + else: + confdir = osp.join(get_module_source_path('spyder.utils.help')) + + confoverrides = {'html_context': context} + + doctreedir = osp.join(srcdir, 'doctrees') + + sphinx_app = Sphinx(srcdir, confdir, srcdir, doctreedir, buildername, + confoverrides, status=None, warning=None, + freshenv=True, warningiserror=False, tags=None) + try: + sphinx_app.build(None, [rst_name]) + except SystemMessage: + output = _("It was not possible to generate rich text help for this " + "object.
    " + "Please see it in plain text.") + return warning(output) + + # TODO: Investigate if this is necessary/important for us + if osp.exists(output_name): + output = codecs.open(output_name, 'r', encoding='utf-8').read() + output = output.replace('
    ', '
    ')
    +    else:
    +        output = _("It was not possible to generate rich text help for this "
    +                    "object.
    " + "Please see it in plain text.") + return warning(output) + + if temp_confdir: + shutil.rmtree(confdir, ignore_errors=True) + shutil.rmtree(srcdir, ignore_errors=True) + + return output + + +def generate_configuration(directory): + """ + Generates a Sphinx configuration in `directory`. + + Parameters + ---------- + directory : str + Base directory to use + """ + + # conf.py file for Sphinx + conf = osp.join(get_module_source_path('spyder.utils.help'), + 'conf.py') + + # Docstring layout page (in Jinja): + layout = osp.join(osp.join(CONFDIR_PATH, 'templates'), 'layout.html') + + os.makedirs(osp.join(directory, 'templates')) + os.makedirs(osp.join(directory, 'static')) + shutil.copy(conf, directory) + shutil.copy(layout, osp.join(directory, 'templates')) + open(osp.join(directory, '__init__.py'), 'w').write('') + open(osp.join(directory, 'static', 'empty'), 'w').write('') diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/utils/help/static/css/default.css spyder-3.0.2+dfsg1/spyder/utils/help/static/css/default.css --- spyder-2.3.8+dfsg1/spyder/utils/help/static/css/default.css 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/utils/help/static/css/default.css 2016-10-25 02:05:23.000000000 +0200 @@ -0,0 +1,415 @@ +body { + background-color: white; + color: rgb(51, 51, 51); + margin: 0px 25px 15px 25px; +} + + +/* --- Title style --- */ +div.title h1 { + font-size: 180%; + font-family: 'Trebuchet MS', sans-serif; + background-color: #6487DC; + background-image: -webkit-gradient( + linear, + 0 0, + 0 100%, + from(#54b4eb), + color-stop(60%, #2fa4e7), + to(#1d9ce5) + ); + text-shadow: 0px 1px 1px rgba(0, 0, 0, 0.2); + font-weight: normal; + padding: 6px 0px 6px 20px; + margin: 0px -25px; + color: #FFFFFF; +} + +/* + * The next two styles are needed to + * modify the anchors present on the + * title of pages like scipy.stats or + * scipy.io + */ +div.title h1 a { + color: transparent; + cursor: default; +} + +div.title h1 tt { + font-size: 95%; + background-color: transparent; + color: #FFFFFF; +} + + +/* --- Metadata style --- */ +div.metadata { + margin-top: 10px; + margin-bottom: 15px; + margin-right: 1px; + padding: 1px; + background-color: #EEEEEE; + border: 1px solid #C9C9C9; + border-radius: 6px 6px 6px 6px; + box-shadow: 1px 1px 7px #CACACA; +} + +div.metadata p { + margin: 7px 0px 7px 10px; +} + +span.def { + font-family: monospace; + font-size: 90%; +} + +span.argspec-highlight { + color: red; + font-size: 110%; + font-weight: 900; +} + + +/* --- Docstring div style --- */ +div.docstring { + margin-top: -1px; +} + +div.docstring p { + padding: 0px 2px 0px; +} + + +/* --- Headers style --- */ +h2, h3, h4 { + font-family: 'Helvetica', sans-serif; + color: rgb(49, 126, 172); + margin-top: 20px; + margin-bottom: 10px; +} + +h2 { + font-size: 140%; + font-weight: normal; + border-bottom: 1px solid rgb(220, 220, 220); + padding: 4px 0px 4px 0px; +} + +h3 { + font-size: 115%; +} + +h4 { + font-size: 100%; + margin-top: 14px; + font-weight: normal; +} + +h2.html-toggle-button, h3.html-toggle-button, h4.html-toggle-button { + padding-left: 20px; +} + +.collapsed > h2, .collapsed > h3, .collapsed > h4, .expanded > h2, .expanded > h3, .expanded > h4 { + background-color: transparent; + background-image: url(../images/collapse_expand.png); + background-repeat: no-repeat; + background-attachment: scroll; + cursor: pointer; +} + +.collapsed > h2 { + background-position: 2px 7px; +} + +.collapsed > h3 { + background-position: 2px 2px; +} + +.collapsed > h4 { + background-position: 2px 0px; +} + +.expanded > h2 { + background-position: 0px -31px; +} + +.expanded > h3 { + background-position: 0px -38px; +} + +.expanded > h4 { + background-position: 0px -39px; +} + +dl.docutils { + padding: 0px 10px 0px; +} + +div.section p { + padding: 0px 2px 0px; +} + +#warning { + margin-top: 5px; + background-color: #FFE4E4; + border: 1px solid #F66; + padding: 4px 8px 4px 8px; + text-align: center; +} + +#doc-warning { + margin-top: 16px; + width: 45%; + margin-left: auto; + margin-right: auto; + color: rgb(185, 74, 72); + background-color: rgb(242, 222, 222); + border: 1px solid rgb(238, 211, 215); + border-radius: 4px 4px 4px 4px; + padding: 15px; + text-align: center; + font-weight: bold; + font-size: 105%; +} + + +/* --- Links --- */ +a { + text-decoration: none; + color: rgba(40, 130, 180, 1); +} + +a:hover { + text-decoration: underline; +} + + +/* --- Images --- */ +img { + box-shadow: 0px 2px 6px #cacaca; + border: 1px solid #c9c9c9; +} + +img.align-center { + display: block; + margin-left: auto; + margin-right: auto; +} + + +/* --- Lists style --- */ +ol.arabic { + margin-left: -10px; +} + +ul { + margin-left: -5px; +} + +/* --- Literal blocks style --- */ +pre.literal-block { + padding-left: 35px; + font-size: 95%; +} + + +/* --- Docutils table style --- */ +table.docutils { + border-collapse: collapse; + border-spacing: 0; + border: #DDDDDD; + margin-left: auto; + margin-right: auto; + margin-top: 17px; + margin-bottom: 17px; + width: 90%; +} + +table.docutils td { + padding: 5px; +} + +table.docutils tr.row-odd { + background-color: rgb(249, 249, 249); +} + + +/* --- Docutils table headers --- */ +table.docutils th { + background-color: #EEEEEE; + border-bottom-color: #DDDDDD; + border-bottom-style: solid; + border-bottom-width: 1px; + border-top-color: #DDDDDD; + border-top-style: solid; + border-top-width: 1px; + font-weight: bold; + text-align: center; + padding: 6px 0px 6px 8px; + color: rgb(65, 65, 65); +} + + +/* --- Field-list table style --- */ +table.docutils.field-list { + font-size: 80%; + border-collapse: collapse; + border-left: transparent; + border-right: transparent; + margin-top: 15px; + margin-left: 40px; + width: 83%; +} + + +/* --- Field-list table headers --- */ +table.docutils.field-list th { + background-color: transparent; + border-top: transparent; + border-bottom: transparent; + color: black; + font-weight: bold; + text-align: left; + padding: 4px 0px 4px 8px; +} + + +/* --- Spacing around example code --- */ +div.highlight pre { + padding: 9px 14px; + background-color: rgb(247, 247, 249); + border-radius: 4px 4px 4px 4px; + border: 1px solid rgb(225, 225, 232); +} + +div.highlight { + padding: 0px 10px 0px; +} + +dt { + font-weight: bold; + /*font-size: 16px;*/ +} + +.classifier { + /*font-size: 10pt;*/ + font-weight: normal; +} + +tt { + background-color: #ECF0F3; + /*font-size: 95%;*/ + padding: 0px 1px; +} + + + +div.admonition.note { + font-size: 0.95em; + margin: 1.3em; + border: 1px solid #BCE8F1; + background-color: #D9EDF7; + padding: 0px 5px 0 5px; + color: #3A87AD; +} + +div.admonition p.admonition-title { + font-size: 1em; + margin-top: 7px; + font-weight: bold; +} + + +/* ----------- Panels ----------- */ + +.panel { + margin-top: 15px; + background-color: #ffffff; + border: 1px solid transparent; + border-radius: 4px; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05); +} + +.panel-heading { + padding: 10px 15px; + border-bottom: 1px solid transparent; + border-top-right-radius: 3px; + border-top-left-radius: 3px; +} + +.panel-title { + margin-top: 0; + margin-bottom: 0; + font-size: 110%; + color: rgb(255,255,255); +} + +.panel-body { + padding: 15px; +} + +.panel-usage { + border-color: #2fa4e7; + margin-top: 15px; + width: 60%; + margin-left: auto; + margin-right: auto; +} + +.panel-usage > .panel-heading { + color: #ffffff; + background-color: #2fa4e7; + border-color: #2fa4e7; +} + +.panel-usage > .panel-body > br { + display: block; + margin: 12px 0; + content: ""; +} + +.hr { + background-color: rgb(200, 200, 200); + height: 1px; +} + + +/* ----------- IPython console styles ----------- */ + +/* --- Loading --- */ +.loading { + position: absolute; + margin: -20px 0 0 -95px; + width: 180px; + height: auto; + left: 50%; + top: 50%; + background-color: #EEEEEE; + border: 1px solid #C9C9C9; + border-radius: 6px; + box-shadow: 0px 0px 7px #CACACA; + color: #333333; + padding: 12px; + text-align: center; +} + +#loading-image { + float: left; +} + +#loading-message { + margin-left: 23px; +} + +/* --- Kernel error messages --- */ +.panel-danger { + border-color: #eed3d7; +} + +.panel-danger > .panel-heading { + color: #b94a48; + background-color: #f2dede; + border-color: #eed3d7; + background-color: rgb(199, 28, 34); +} diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/utils/help/static/css/pygments.css spyder-3.0.2+dfsg1/spyder/utils/help/static/css/pygments.css --- spyder-2.3.8+dfsg1/spyder/utils/help/static/css/pygments.css 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/utils/help/static/css/pygments.css 2016-10-25 02:05:23.000000000 +0200 @@ -0,0 +1,61 @@ +.hll { background-color: #ffffcc } +.c { color: #408090; font-style: italic } /* Comment */ +.err { border: 1px solid #FF0000 } /* Error */ +.k { color: #007020; font-weight: bold } /* Keyword */ +.o { color: #666666 } /* Operator */ +.cm { color: #408090; font-style: italic } /* Comment.Multiline */ +.cp { color: #007020 } /* Comment.Preproc */ +.c1 { color: #408090; font-style: italic } /* Comment.Single */ +.cs { color: #408090; background-color: #fff0f0 } /* Comment.Special */ +.gd { color: #A00000 } /* Generic.Deleted */ +.ge { font-style: italic } /* Generic.Emph */ +.gr { color: #FF0000 } /* Generic.Error */ +.gh { color: #000080; font-weight: bold } /* Generic.Heading */ +.gi { color: #00A000 } /* Generic.Inserted */ +.go { color: #303030 } /* Generic.Output */ +.gp { color: #c65d09; font-weight: bold } /* Generic.Prompt */ +.gs { font-weight: bold } /* Generic.Strong */ +.gu { color: #800080; font-weight: bold } /* Generic.Subheading */ +.gt { color: #0040D0 } /* Generic.Traceback */ +.kc { color: #007020; font-weight: bold } /* Keyword.Constant */ +.kd { color: #007020; font-weight: bold } /* Keyword.Declaration */ +.kn { color: #007020; font-weight: bold } /* Keyword.Namespace */ +.kp { color: #007020 } /* Keyword.Pseudo */ +.kr { color: #007020; font-weight: bold } /* Keyword.Reserved */ +.kt { color: #902000 } /* Keyword.Type */ +.m { color: #208050 } /* Literal.Number */ +.s { color: #4070a0 } /* Literal.String */ +.na { color: #4070a0 } /* Name.Attribute */ +.nb { color: #007020 } /* Name.Builtin */ +.nc { color: #0e84b5; font-weight: bold } /* Name.Class */ +.no { color: #60add5 } /* Name.Constant */ +.nd { color: #555555; font-weight: bold } /* Name.Decorator */ +.ni { color: #d55537; font-weight: bold } /* Name.Entity */ +.ne { color: #007020 } /* Name.Exception */ +.nf { color: #06287e } /* Name.Function */ +.nl { color: #002070; font-weight: bold } /* Name.Label */ +.nn { color: #0e84b5; font-weight: bold } /* Name.Namespace */ +.nt { color: #062873; font-weight: bold } /* Name.Tag */ +.nv { color: #bb60d5 } /* Name.Variable */ +.ow { color: #007020; font-weight: bold } /* Operator.Word */ +.w { color: #bbbbbb } /* Text.Whitespace */ +.mf { color: #208050 } /* Literal.Number.Float */ +.mh { color: #208050 } /* Literal.Number.Hex */ +.mi { color: #208050 } /* Literal.Number.Integer */ +.mo { color: #208050 } /* Literal.Number.Oct */ +.sb { color: #4070a0 } /* Literal.String.Backtick */ +.sc { color: #4070a0 } /* Literal.String.Char */ +.sd { color: #4070a0; font-style: italic } /* Literal.String.Doc */ +.s2 { color: #4070a0 } /* Literal.String.Double */ +.se { color: #4070a0; font-weight: bold } /* Literal.String.Escape */ +.sh { color: #4070a0 } /* Literal.String.Heredoc */ +.si { color: #70a0d0; font-style: italic } /* Literal.String.Interpol */ +.sx { color: #c65d09 } /* Literal.String.Other */ +.sr { color: #235388 } /* Literal.String.Regex */ +.s1 { color: #4070a0 } /* Literal.String.Single */ +.ss { color: #517918 } /* Literal.String.Symbol */ +.bp { color: #007020 } /* Name.Builtin.Pseudo */ +.vc { color: #bb60d5 } /* Name.Variable.Class */ +.vg { color: #bb60d5 } /* Name.Variable.Global */ +.vi { color: #bb60d5 } /* Name.Variable.Instance */ +.il { color: #208050 } /* Literal.Number.Integer.Long */ \ Kein Zeilenumbruch am Dateiende. Binärdateien spyder-2.3.8+dfsg1/spyder/utils/help/static/images/collapse_expand.png und spyder-3.0.2+dfsg1/spyder/utils/help/static/images/collapse_expand.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/utils/help/static/images/debug-continue.png und spyder-3.0.2+dfsg1/spyder/utils/help/static/images/debug-continue.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/utils/help/static/images/debug-step-in.png und spyder-3.0.2+dfsg1/spyder/utils/help/static/images/debug-step-in.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/utils/help/static/images/debug-step-out.png und spyder-3.0.2+dfsg1/spyder/utils/help/static/images/debug-step-out.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/utils/help/static/images/debug-step-over.png und spyder-3.0.2+dfsg1/spyder/utils/help/static/images/debug-step-over.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/utils/help/static/images/spyder-hello-docstring.png und spyder-3.0.2+dfsg1/spyder/utils/help/static/images/spyder-hello-docstring.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/utils/help/static/images/spyder-nice-docstring-rendering.png und spyder-3.0.2+dfsg1/spyder/utils/help/static/images/spyder-nice-docstring-rendering.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/utils/help/static/images/spyder-sympy-example.png und spyder-3.0.2+dfsg1/spyder/utils/help/static/images/spyder-sympy-example.png sind verschieden. diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/utils/help/templates/layout.html spyder-3.0.2+dfsg1/spyder/utils/help/templates/layout.html --- spyder-2.3.8+dfsg1/spyder/utils/help/templates/layout.html 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/utils/help/templates/layout.html 2016-10-25 02:05:23.000000000 +0200 @@ -0,0 +1,85 @@ +{# + layout.html + ~~~~~~~~~~~ + + Layout template for the Help plugin + + :copyright: Copyright (c) Spyder Project Contributors. + :copyright: Copyright 2009 by Tim Dumol + :license: BSD license +#} + + + + + + + + + + + + {% if right_sphinx_version and math_on %} + {# DON'T try to load MathJax from the net. It's slow and sometimes gives + errors. See this thread for more info: + http://tex.stackexchange.com/questions/2692/comparing-mathjax-and-mathml + #} + + {% endif %} + + + + + + + + +{% if collapse %} + + +{% endif %} + +{% if img_path %} + +{% endif %} + + + + {# Docstring header #} + {% if name %} +

    {{name}}

    + + {% if argspec or note %} + + {% endif %} + + {% endif %} + + {# Docstring text #} +
    + {% block body %}{% endblock %} + {% if collapse %} +
    +

    Outline

    + {{ toc }} +
    + {% endif %} +
    + + + diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/utils/help/templates/usage.html spyder-3.0.2+dfsg1/spyder/utils/help/templates/usage.html --- spyder-2.3.8+dfsg1/spyder/utils/help/templates/usage.html 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/utils/help/templates/usage.html 2016-10-25 02:05:23.000000000 +0200 @@ -0,0 +1,39 @@ +{# + usage.html + ~~~~~~~~~~ + + A simple page to inform users how to get help on the Help plugin + + :copyright: Copyright (c) Spyder Project Contributors. + :license: MIT license +#} + + + + + + + + + + + +
    +
    +
    {{title}}
    +
    +
    + {{intro_message}} +

    +
    +
    +
    + {{tutorial_message}} + {{tutorial}} +
    +
    +
    + + + diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/utils/help/templates/warning.html spyder-3.0.2+dfsg1/spyder/utils/help/templates/warning.html --- spyder-2.3.8+dfsg1/spyder/utils/help/templates/warning.html 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/utils/help/templates/warning.html 2016-10-25 02:05:23.000000000 +0200 @@ -0,0 +1,26 @@ +{# + warning.html + ~~~~~~~~~~~ + + A simple page to emit a warning when no docstring text was found for the + one a user was looking for + + :copyright: Copyright (c) Spyder Project Contributors + :license: MIT license +#} + + + + + + + + + + + +
    {{text}}
    + + + diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/utils/help/tutorial.rst spyder-3.0.2+dfsg1/spyder/utils/help/tutorial.rst --- spyder-2.3.8+dfsg1/spyder/utils/help/tutorial.rst 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/utils/help/tutorial.rst 2016-10-25 02:05:23.000000000 +0200 @@ -0,0 +1,950 @@ +====================================================== +Spyder - the Scientific PYthon Development EnviRonment +====================================================== + +*Spyder* is an Integrated Development Environment (IDE) for scientific +computing using the Python programming language. It comes with an +Editor to write code, a Console to evaluate it and see its results at +any time, a Variable Explorer to see what variables have been defined +during evaluation, and several other facilities to help you to +effectively develop the programs you need as a scientist. + + +This tutorial is authored by +`Hans Fangohr `__ from the +University of Southampton (UK) (see `historical note`_ for more +detail). + + +First steps with Spyder +####################### + +This section is aimed at Python and Spyder beginners. If you find it too +simple, please continue to the next section. + +Execute a given program +----------------------- + +* We are going to use this program as a first example: + + .. code-block:: python + + # Demo file for Spyder Tutorial + # Hans Fangohr, University of Southampton, UK + + def hello(): + """Print "Hello World" and return None""" + print("Hello World") + + # main program starts here + hello() + +* To use this program, please create a new file in the Spyder editor pane. Then copy + and paste the code inside the box above on the file, and the save it with the name + ``hello.py``. + +* To execute the program, select ``Run > Run`` from the menu (or press F5), and + confirm the ``Run settings`` if required. + + If this is your first time, you should see an output like this:: + + In [1]: runfile('/Users/fangohr/Desktop/hello.py', wdir=r'/Users/fangohr/Desktop') + Hello World + + In [2]: + + If so, then you have just run your first Python program - well done. + + .. note:: + + The particular path shown next to ``runfile`` will depend on where you have saved + the file, but this is inserted by Spyder automatically. + + +Use the IPython Console +~~~~~~~~~~~~~~~~~~~~~~~ + +Before we proceed, we recommend you to use the IPython console. This console can do a +little more than the standard Python console, and we suggest to use it as the default +console here. + + +What happens when you execute the program? +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +* Python reads the file line by line, ignoring comments (i.e. lines starting + with the ``#`` symbol). + +* When it comes across the ``def`` keyword, it knows that a function + is DEFined in this and the next (one or more) lines. All *indented* lines + following ``def hello():`` belong to the function body. + + Note that the function object is just created at this point in the + file, but the function is not yet called (i.e. not executed). + +* When Python comes across commands (other than ``def ...`` and a few + other keywords) that are written in the left-most column, it will + execute these immediately. In the ``hello.py`` file this is only the + line reading ``hello()`` which will actually call (i.e. *execute*) + the function with name ``hello``. + + If you comment or remove the line ``hello()`` from the program and run + the whole file again (by pressing F5, or selecting ``Run > Run``), nothing + will be printed (because the function ``hello`` is defined, but not called, + i.e. not executed). + + +Now you should know how to execute a Python program that you have in +the editor pane in Spyder using the IPython console. + +If you are just starting to learn Python, this is probably a good +point to return to your text book / course and look at more basic +examples. + + +The next section gives more detailed information how you can execute +*parts* of the code in the editor in the IPython console, and thus +update parts of your definitions in the editor. This is a more +advanced technique but can be very useful. (You may also be interested +in the option to execute chunks (so-called "cells") of code that are +separated by delimiters -- see `Shortcuts for useful functions`_.) + + + +Call existing functions in the console +-------------------------------------- + +Once you have executed the ``hello.py`` program, the function object ``hello`` +is defined and known to the IPython console. We can thus call the function from +the console like this: + +* Type ``hello()`` in the console (next to ``In [?]`` prompt, where + the question mark can be any positive integer number), and press the + ``Enter`` key. + + You should find that the ``hello()`` function is executed again, + i.e. ``Hello World`` is printed again. Your function call at the + console together with the output should look like this:: + + In [ ]: hello() + Hello World + +* Can you see how this differs from executing the whole program again? + + When we execute the whole program (by pressing F5), Python goes + through the file, creates the ``hello`` function object (overriding + the previous object), reaches the ``hello()`` line and calls the + function. + + When we call ``hello()`` in the console, we only call the + function object ``hello`` that has been defined in the IPython + console when we executed the whole ``hello.py`` file earlier (by + pressing F5). + + This will become clearer over time and also when we work with + slightly larger examples. You may want to return to this tutorial at + a slightly later stage. + + +Inspecting objects defined in the console +----------------------------------------- + +* Python provides a function that displays all known objects in the + current name space of the console. It is called ``dir()``: when you + type ``dir()`` at the console, you get a list of known objects. Ignore + everything starting with an underscore for now. Can you see ``hello`` + in the list? + + .. note:: + + If you get a long list of defined objects, then Spyder may have + done some convenience imports for you already. To address this you + may want to: + + - `Reset the name space`_ + + - Execute ``hello.py`` again by pressing F5 + + Then run ``dir()`` as suggested above. + +* Once an object is visible in the current name space (as is ``hello`` + in this example), we can use the ``help`` function as follows to + learn about it: typing ``help(hello)`` at the console prompt, you + should see an output like this:: + + In [ ]: help(hello) + Help on function hello in module __main__: + + hello() + Print "Hello World" and return None + + + Where does Python take the information from? Some of it (like the + number of input arguments and names of those variables; here we have + no input arguments) Python can find through inspecting its objects, + additional information comes from the documentation string provided + for the function object ``hello``. The documentation string is the + first string immediately below the line ``def hello():``. + + These strings are special, and they are called *docstrings* which is short for + *documentation strings*. As they usually extend over multiple lines, there + are enclosed by triple single quotes (``'''``) or triple double quotes + (``"""``). + +* The Spyder environment also provides a ``Help`` pane which + by default is located in the top right corner. + + While the cursor is on the name of an object, + press ``CTRL+i`` (or ``CMD+i`` on Mac), and you should find that + the same information as we obtained from ``help(hello)`` is provided + automatically in the Help: + + .. image:: images/spyder-hello-docstring.png + :align: center + + This works in the console and in the editor. + +Updating objects +---------------- + +Simple strategy: re-execute whole program +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +* In the Editor window, change the function ``hello`` so + that it prints ``Good Bye World`` rather than ``Hello World``. + +* Press F5 (to execute the whole program) and check that the output of the + program is now:: + + Good Bye World + +What has happened when you pressed F5 is this: Python has gone through +the ``hello.py`` file and created a new function object ``hello`` +(overriding the function object ``hello`` we had defined before) and +then executed the function. + + +Looking at the details +~~~~~~~~~~~~~~~~~~~~~~ + +We need to start with a clearly defined state. To do this, please change the +function ``hello()`` back so that it prints ``Hello World``, then press +F5 to run the whole program and check that it prints ``Hello World``. + +* Call the function ``hello()`` from the command prompt (as described + in `Call existing functions in the console`_). You + should see ``Hello World`` printed. + +* Now change the function definition so that it would print ``Laters + World``, and save the file (but do NOT execute the program, i.e. do + NOT press F5 yet). + +* Call the function ``hello()`` in the console again. You + should find that the text printed reads ``Hello World``, like here + :: + + In [ ]: hello() + Hello World + + Why is this so? Because the ``hello`` function object in the console + is the old one which prints ``Hello World``. So far, we have + changed the file ``hello.py`` (and replaced ``Hello World`` in there with + ``Laters World``) in the editor but this has not affected the objects that + have previously been created in the console. + +Here are two possibilities to use our modified version of the ``hello`` +function: + +* Option 1: execute the whole file ``hello.py`` again by pressing F5: + this creates a new function object ``hello`` (and overrides the old + one). You should find that if you press F5, and then call + ``hello()`` at the prompt, the new text ``Laters World`` is printed. + +* Option 2: select the region you have changed (in this case the whole + function ``hello``, starting from the line ``def hello():`` down to + ``print("Laters Wold")``, and then select ``Run > Run selection``. + + This will update the ``hello`` object in the console without + having to execute the whole ``hello.py`` file:: + + In [ ]: def hello(): + ...: """Print "Hello World" and return None""" + ...: print("Laters world") + ...: + + If we now type ``hello()``, we see the update response:: + + In [ ]: hello() + Laters world + +The ability to execute *parts of the code* to update some objects in +the console (in the example above, we updated the function object +``hello``), is of great use when developing and debugging more complex +codes, and when creating objects/data in the console session take +time. For example, by modifying only the functions (or +classes/objects, etc.) that we are actually developing or debugging, we +can keep re-using the data and other objects that are defined in the +console session. + + + +Recommended first steps for Python beginners +############################################ + +To teach and learn Python programming, we recommend here to use IPython +instead of the normal Python console. This accepts IPython as the +de-facto standard in the scientific Python community. + +Switch to an IPython console +---------------------------- + +If you already have an IPython console active, you can ignore this section, +and make it visible by clicking on the "IPython console" rider. + +In the console window (lower right corner by default), you see by +default a prompt with three greater than signs, i.e. ``>>>``. This +shows that we are using the ``console`` -- basically a normal Python +console session (with some added functionality from Spyder). + +Instead, we would like to use an *Interactive Python* console, short *IPython* +from the `IPython project `__. To do this, select +``Consoles > Open an IPython Console``. + +You should see in the consolse window a new shell appearing, and the +IPython prompt ``In [1]:`` should be displayed. + +Reset the name space +-------------------- + +The `name space `__ +(i.e. the collection of objects defined in the console at any given time) +can be cleared in IPython using the ``%reset`` command. Type ``%reset`` +and press return, then confirm with ``y``:: + + In [1]: %reset + + Once deleted, variables cannot be recovered. Proceed (y/[n])? y + + In [2]: + +That's all. + +We discuss this a little further, but you can skip the following if +you are not interested: after issuing the ``%reset`` command, we +should have only a few objects defined in the name space of that +session. We can list all of them using the ``dir()`` command:: + + In [2]: dir() + Out[2]: + ['In', + 'Out', + '__builtin__', + '__builtins__', + '__name__', + '_dh', + '_i', + '_i2', + '_ih', + '_ii', + '_iii', + '_oh', + '_sh', + 'exit', + 'get_ipython', + 'help', + 'quit'] + +Finally, if you like to skip the confirmation step of the ``reset`` +command, use can use ``%reset -f`` instead of ``%reset``. + +Strive for PEP8 Compliance +-------------------------- + +In addition to the syntax that is enforced by the Python programming +language, there are additional conventions regarding the layout of +the source code, in particular the `Style Guide for Python source code +`__ known as "PEP8". By +following this guide and writing code in the same style as almost all +Python programmers do, it becomes easier to read, and thus easier to +debug and re-use -- both for the original author and others. + +You should change Spyders settings to +`Warn if PEP8 coding guidelines are violated`_. + + + +Selected Preferences +#################### + +Where are the preferences? +-------------------------- + +A lot of Spyder's behaviour can be configured through it's +Preferences. Where this is located in the menu depends on your +operating system: + +* On Windows and Linux, go to ``Tools > Preferences`` + +* On Mac OS, go to ``Python/Spyder > Preferences`` + +Warn if PEP8 coding guidelines are violated +------------------------------------------- + +Go to ``Preferences > Editor > Code +Introspection/Analysis`` and +select the tickbox next to ``Style analysis (PEP8)`` + +Automatic Symbolic Python +------------------------- + +Through ``Preferences > IPython console > Advanced Settings > Use +symbolic math`` we can activate IPython's SYMbolic PYthon (sympy) mode that is +provided by the `sympy `__ module. This mode +in Spyder allows nicely rendered mathematical output (LaTeX style) and also +imports some sympy objects automatically when the IPython console starts, and +reports what it has done. + +.. code-block:: python + + These commands were executed: + >>> from __future__ import division + >>> from sympy import * + >>> x, y, z, t = symbols('x y z t') + >>> k, m, n = symbols('k m n', integer=True) + >>> f, g, h = symbols('f g h', cls=Function) + +We can then use the variables ``x``, ``y``, for example like this: + +.. image:: images/spyder-sympy-example.png + :align: center + + + +Shortcuts for useful functions +############################## + +- ``F5`` executes the current file + +- ``F9`` executes the currently highlighted chunk of code: this is very useful + to update definitions of functions (say) in the console session without + having to run the whole file again. If nothing is selected ``F9`` executes + the current line. + +- ``Tab`` auto-completes commands, function names, variable + names, methods in the Console (both Python and IPython) and in the + Editor. This feature is very useful, and should be used + routinely. Do try it now if auto-completion is new to you. + Assume you have defined a variable:: + + mylongvariablename = 42 + + Suppose we need to write code that computes ``mylongvariablename + + 100``, we can simply type ``my`` and then press the ``Tab`` key. The + full variable name will be completed and inserted at the cursor + position if the name is unique, and then we can carry on and type + ``+ 100``. If the name is not uniquely identifiable given the + letters ``my``, a list field will be displayed from which the right + variable can be chosen. Choosing from the list can be done with the + ```` key and ```` key and the ``Enter`` + key to select, or by typing more letters of the name in question + (the selection will update automatically) and confirming by pressing + ``Enter`` when the right name is identified. + +- ``Ctrl+Enter`` executes the current cell (menu entry ``Run > Run + cell``). A cell is defined as the code between two lines which start with + the agreed tag ``#%%``. + +- ``Shift+Enter`` executes the current cell and advances the + cursor to the next cell (menu entry ``Run > Run cell and + advance``). + + Cells are useful to execute a large file/code segment in smaller + units. (It is a little bit like a cell in an IPython notebook, in + that chunks of code can be run independently.) + +- ``Alt+`` moves the current line up. If multiple lines are + highlighted, they are moved up together. ``Alt+`` + works correspondingly moving line(s) down. + +- ``Ctrl+Left Mouse Click`` on a function/method in the source, opens a new + editor windows showing the definition of that function. + +- ``Shift+Ctrl+Alt+M`` maximizes the current window (or changes the + size back to normal if pressed in a maximized window) + +- ``Ctrl+Shift+F`` activates the search pane across all files. + +- ``Cmd + +`` (On MacOS X) or ``Ctrl + +`` (otherwise) will increase the font + size in the Editor, whereas ``Cmd + -`` (``Ctrl + -``) will decrease it. + Also works in the IPython Console. + + The font size for the Help, the Python console etc. can be set + individually via ``Preferences > Help`` etc. + + I couldn't find a way of changing the font size in the variable explorer. + +- ``Cmd+S`` (on MacOS X) and ``Ctrl+S`` (otherwise) *in the Editor* + pane saves the file + currently being edited. This also forces various warning triangles + in the left column of the Editor to be updated (otherwise they + update every 2 to 3 seconds by default). + +- ``Cmd+S`` (on MacOS X) and ``Ctrl+S`` (otherwise) *in the IPython console* + pane saves the current IPython session as an HTML file, including + any figures that may be displayed inline. This is useful as a quick + way of recording what has been done in a session. + + (It is not + possible to load this saved record back into the session - if you + need functionality like this, look for the IPython Notebook.) + +- ``Cmd+I`` (on Mac OS X) and ``Ctrl+I`` (otherwise) when pressed + while the cursor is on an object, opens documentation for that + object in the help pane. + + + +Run Settings +############ + +These are the settings that define how the code in the editor is +executed if we select ``Run > Run`` or press F5. + +By default, the settings box will appear the first time we try to execute a +file. If we want to change the settings at any other time, they can be +found under ``Run > Configure`` or by pressing F6. + +There are three choices for the console to use, of which I'll discuss the +first two. Let's assume we have a program ``hello.py`` in the editor which +reads + +.. code-block:: python + + def hello(name): + """Given an object 'name', print 'Hello ' and the object.""" + print("Hello {}".format(name)) + + + i = 42 + if __name__ == "__main__": + hello(i) + +Execute in current Python or IPython console +-------------------------------------------- + +This is the default suggestion, and also generally a good choice. + +Persistence of objects I (after code execution) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Choosing ``Execute in current Python or IPython console`` +setting under ``Run > Configure`` means that + +1. When the execution of ``hello.py`` is completed, we can interact + with the console in which the program ran, and we can use the + convenient IPython console for this (rather than the default + Python console). + + In particular, + +2. we can inspect and interact with objects that the execution of + our program created, such as ``i`` and ``hello()``. + +This is generally very useful for incremental coding, testing and +debugging: we can call ``hello()`` directly from the console +prompt, and don't need to execute the whole ``hello.py`` for this +(although if we change the function ``hello()``, we need to execute +the file, or at least the function definition, to make the new +version of ``hello()`` visible at the console; either by +executing the whole buffer or via ``Run > Run Selection``.) + +Persistence of objects II (from before code execution) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +However, executing the code in the editor in the current console +also means that + +3. the code that executes can see other (global) objects that were + defined in the console session. + +*This* persistence of objects is easily forgotten and usually not +required when working on small programs (although it can be of great +value occasionally). These objects could come from previous execution +of code, from interactive work in the console, or from convenience +imports such as ``from pylab import *`` (Spyder may do some of those +convenience imports automatically). + +This visibility of objects in the console name space to the +code we execute may also result in coding mistakes if the code +inadvertently relies on these objects. + +Here is an example: imagine that + +* we run the code ``hello.py``. Subsequently, the variable ``i`` + is known in the console as a global variable. + +* we edit the ``hello.py`` source and accidentally delete the line + ``i = 42`` + +* we execute the buffer containing ``hello.py`` again. At this + point, the call of ``hello(i)`` will *not* fail because the + console has an object of name ``i`` defined, although this is + not defined in the source of ``hello.py``. + +At this point, we could save ``hello.py`` and (falsely) think it +would execute correctly. However, running it in a new (I)Python +console session (or via ``python hello.py`` in a terminal, say) +would result in an error, because ``i`` is not defined. + +The problem arises because the code makes use of an object (here +``i``) without creating it. This also affects importing of modules: if +we had imported ``pylab`` at the IPython prompt, then our program will +see that when executed in this IPython console session. + +To learn how we can double check that our code does not depend on such +existing objects, see `How to double check your code executes correctly "on +its own"`_ . + +Execute in new dedicated Python console +--------------------------------------- + +Choosing ``Execute in new dedicated Python console`` under ``Run +> Configure`` will start *a new Python console every time* the +``hello.py`` program is executed. The major advantage of this mode +over `Execute in current Python or IPython console`_ is that we +can be certain that there are no global objects defined in this +console which originate from debugging and repeated execution of +our code: every time we run the code in the editor, the python +console in which the code runs is restarted. + +This is a safe option, but provides less flexibility and cannot use +the IPython console. + +How to double check your code executes correctly "on its own" +------------------------------------------------------------- + +Assuming you have chosen for your code to +`Execute in current Python or IPython console`_, +then you have two options to check that your code does work on its own +(i.e. it does not depend on undefined variables, unimported modules +and commands etc.) + +(i) Switch from `Execute in current Python or IPython console`_ + to `Execute in new dedicated Python console`_, + and execute the code in the editor in this dedicated Python console. + + Alternatively, if you want to stay with the current IPython + console, you can + +(ii) Use IPython's magic ``%reset`` command which will remove all + objects (such as ``i`` in the example above) from the current name + space, and then execute the code in the editor. + +Recommendation +-------------- + +My recommendation for beginners would be to +`Execute in current Python or IPython console`_, *and* +to choose the IPython console for this. + +Once you have completed a piece of code, double check that it executes +independently using one of the options explained in +`How to double check your code executes correctly "on its own"`_\ . + + + +Other observations +################## + +Multiple files +-------------- + +When multiple files are opened in the Editor, the +corresponding tabs at the top of the window area are arranged in +alphabetical order of the filename from left to right. + +On the left of the tabs, there is as icon that shows ``Browse tabs`` +if the mouse hovers over it. It is useful to jump to a particular +file directly, if many files are open. + +Environment variables +--------------------- + +Environment variables can be displayed from the Python Console window (bottom +right window in default layout). Click on the ``Options`` icon (the tooltip is +``Options``), then select ``Environment variables``. + +Reset all customization +----------------------- + +All customization saved on disk can be reset by calling Spyder from +the command line with the switch ``--reset``, i.e. a command like +``spyder --reset``. + +Objects in the variable explorer +-------------------------------- + +Right-clicking on arrays in the variable explorer gives options to +plot and analyze these further. + +Double clicking on a dictionary object opens a new window that +displays the dictionary nicely. + +You can also show and edit the contents of numpy arrays, lists, numbers +and strings. + + + +Documentation string formatting +############################### + +If you want to add documentation for the code you are developing, we recommend +you to write documentation strings (or *docstrings*) for it, using a special +format called reStructuredText (`quick reference +`__). This format +also needs to follow a set of conventions called the `Numpydoc standard +`__ + +If you follow those guidelines, you can obtain beautifully formatted docstrings +in Spyder. + +For example, to get an ``average()`` function look like this in the +Spyder Help pane: + +.. image:: images/spyder-nice-docstring-rendering.png + :align: center + +you need to format the documentation string as follows + +.. code-block:: python + + def average(a, b): + """ + Given two numbers a and b, return their average value. + + Parameters + ---------- + a : number + A number + b : number + Another number + + Returns + ------- + res : number + The average of a and b, computed using 0.5*(a + b) + + Example + ------- + >>> average(5, 10) + 7.5 + + """ + + return (a + b) * 0.5 + +What matters here, is that the word ``Parameters`` is used, and +underlined. The line ``a : number`` shows us that the type of the +parameter ``a`` is ``number``. In the next line, which is indented, we +can write a more extended explanation of what this variable represents, +what conditions the allowed types have to fulfill, etc. + +The same for all Parameters, and also for the returned value. + +Often it is a good idea to include an example too, as shown. + + + +Debugging +######### + +Line by line step execution of code +----------------------------------- + +Activating the debug mode (with the ``Debug > Debug`` menu option or Ctrl+F5) +starts the Python debugger (Pdb) if the Python console is active, or the IPython +debugger (ipdb) if the IPython console is active. After doing that, the +Editor pane will highlight the line that is about to be executed, and the +Variable Explorer will display variables in the current context of the point +of program execution. (It only displays 'numerical' and array type of variables, +i.e. not function or class objects) + +After entering debug mode, you can execute the code line by line using the +``Step`` button of the Debug toolbar: + +.. image:: images/debug-step-over.png + :align: center + +or the shortcut Ctrl+F10. You can also inspect how a particular function is +working by stepping into it with the ``Step into`` button + +.. image:: images/debug-step-in.png + :align: center + +or the shortcut Ctrl+F11. Finally, to get out of a function and continue with +the next line you need to use the ``Step return`` button + +.. image:: images/debug-step-out.png + :align: center + +or the shortcut Ctrl+F12. + +If you prefer to inspect your program at a specific point, you need to insert a +*breakpoint* by pressing F12 on the line on which you want to stop. After +that a red dot will be placed next to the line and you can press the ``Continue`` +button + +.. image:: images/debug-continue.png + :align: center + +(after entering debug mode) to stop the execution at that line. + +.. note:: + + You can also control the debugging process by issuing these commands in the + console prompt: + + * ``n`` to move to the Next statement. + + * ``s`` to Step into the current statement. If this is a function + call, step into that function. + + * ``r`` to complete all statements in the current function and Return + from that function before returning control. + + * ``p`` to print values of variables, for example ``p x`` will print the + value of the variable ``x``. + +At the debugger prompt, you can also *change* values of variables. For +example, to modify a variable ``x`` at the IPython debugger prompt, you can say +``ipdb > x = 42`` and the debugger will carry on with ``x`` being bound to ``42``. +You can also call functions, and do many others things. Try this example:: + + def demo(x): + for i in range(5): + print("i={}, x={}".format(i, x)) + x = x + 1 + + demo(0) + +If we execute this (``Run > Run``), we should see the output:: + + i=0, x=0 + i=1, x=1 + i=2, x=2 + i=3, x=3 + i=4, x=4 + +Now execute this using the debugger (``Debug > Debug``), press the +``Step button`` until the highlighted line reaches the ``demo(0)`` +function call, then press the ``Step into`` to inspect this function. +Keep pressing the ``Step button`` to execute the next lines. Then, +modify ``x`` by typing ``x=10`` in the debugger prompt. You see x +changing in the Variable Explorer. You should also see ``x`` changing +when its value is printed as part of the ``demo()`` function. (The +printed output appears between your debugger commands and responses.) + +This debugging ability to execute code line by line, to inspect variables as +they change, and to modify them manually is a powerful tool to +understand what a piece of code is doing (and to correct it if desired). + +To leave the debugging mode, you can type ``exit`` or select from the +menu ``Debug > Debugging Control > Exit`` + +Debugging once an exception has occurred with IPython +----------------------------------------------------- + +In the IPython console, we can call ``%debug`` +straight after an exception has been raised: this will start the +IPython debug mode, which allows inspection of local variables at the +point where the exception occurred as described above. This is a lot +more efficient than adding ``print`` statements to the code an +running it again. + +If you use this, you may also want to use the commands ``up`` +(i.e. press ``u`` at the debugger) and ``down`` (i.e. press ``d``) which +navigate the inspection point up and down the stack. (Up the stack means +to the functions that have called the current function; down is the +opposite direction.) + + + +Plotting +######## + +Plotting with the IPython console +--------------------------------- + +Assuming we use an IPython console with version >= 1.0.0, we can +decide whether figures created with matplotlib/pylab will show + +1. *inline*, i.e. inside the IPython console, or whether they should + +2. appear inside a new window. + +Option 1 is convenient to save a record of the interactive session +(section `Shortcuts for useful functions`_ lists a shortcut to save +the IPython console to an html file). + +Option 2 allows to interactively zoom into the figure, manipulate it a little, +and save the figure to different file formats via a menu the window it +contains has. + +The command to get the figures to appear *inline* in the IPython +console is:: + + In [3]: %matplotlib inline + +The command to get figures appear in their own window (which +technically is a Qt window) is:: + + In [4]: %matplotlib qt + +The Spyder preferences can be used to customize the default behavior +(in particular ``Preferences > IPython Console > Graphics > +Activate Support`` to switch into inline plotting). + +Here are two lines you can use to quickly create a plot and test +this:: + + In [5]: import pylab + In [6]: pylab.plot(range(10), 'o') + + +Plotting with the Python console +-------------------------------- + +If we use the Python console, all plots will appear in a new window +(there is no way of making it appear inline inside the Python +console - this only works for the IPython Console). + +Here is a brief example that you can use to create and display a +plot:: + + >>> import pylab + >>> pylab.plot(range(10), 'o') + +If you execute your code in a dedicated console, you need to use +matplotlib's or pylab's ``show()`` command in your code to make a plot +appear, like this: ``pylab.show()``. + +Note that the ``show()`` command will bind the focus to new window +that has appeared, and that you will need to close that window before +Spyder can accept any further commands or respond to interaction. If +you cannot see the new window, check whether it may have appeared behind +the Spyder window, or be partly hidden. + + + +Historical note +############### + +This tutorial is based on `notes +`__ +by `Hans Fangohr `__, that are +used at the `University of Southampton `__ to +`teach Python for computational modelling +`__ to +undergraduate engineers and postgraduate PhD students for the +`Next Generation Computational Modelling `__ +doctoral training centre. diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/utils/icon_manager.py spyder-3.0.2+dfsg1/spyder/utils/icon_manager.py --- spyder-2.3.8+dfsg1/spyder/utils/icon_manager.py 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/utils/icon_manager.py 2016-10-25 02:05:23.000000000 +0200 @@ -0,0 +1,267 @@ +# -*- coding: utf-8 -*- +# +# Copyright © Spyder Project Contributors +# Licensed under the terms of the MIT License +# (see spyder/__init__.py for details) + +# Standard library imports +import os.path as osp + +# Third party imports +from qtpy.QtGui import QIcon +from qtpy.QtWidgets import QStyle, QWidget + +# Local imports +from spyder.config.base import get_image_path +from spyder.config.main import CONF +import qtawesome as qta + + +_resource = { + 'directory': osp.join(osp.dirname(osp.realpath(__file__)), '../fonts'), + 'loaded': False, +} + +_qtaargs = { + 'log': [('fa.file-text-o',), {}], + 'configure': [('fa.wrench',), {}], + 'bold': [('fa.bold',), {}], + 'italic': [('fa.italic',), {}], + 'genprefs': [('fa.cogs',), {}], + 'exit': [('fa.power-off',), {}], + 'run_small': [('fa.play',), {'color': 'green'}], + 'stop': [('fa.stop',), {'color': 'darkred'}], + 'syspath': [('fa.cogs',), {}], + 'font': [('fa.font',), {}], + 'keyboard': [('fa.keyboard-o',), {}], + 'eyedropper': [('fa.eyedropper',), {}], + 'tooloptions': [('fa.cog',), {'color': '#333333'}], + 'edit24': [('fa.edit',), {}], + 'edit': [('fa.edit',), {}], + 'filenew': [('fa.file-o',), {}], + 'fileopen': [('fa.folder-open',), {}], + 'revert': [('fa.undo',), {}], + 'filesave': [('fa.save',), {}], + 'save_all': [('fa.save', 'fa.save'), {'options': [{'offset': (-0.2, -0.2), 'scale_factor': 0.6}, {'offset': (0.2, 0.2), 'scale_factor': 0.6}]}], + 'filesaveas': [('fa.save', 'fa.pencil'), {'options': [{'offset': (-0.2, -0.2), 'scale_factor': 0.6}, {'offset': (0.2, 0.2), 'scale_factor': 0.6}]}], + 'print': [('fa.print',), {}], + 'fileclose': [('fa.close',), {}], + 'filecloseall': [('fa.close', 'fa.close', 'fa.close'), {'options': [{'scale_factor': 0.6, 'offset': (0.3, -0.3)}, {'scale_factor': 0.6, 'offset': (-0.3, -0.3)}, {'scale_factor': 0.6, 'offset': (0.3, 0.3)}]}], + 'breakpoint_big': [('fa.circle',), {'color': 'darkred'} ], + 'breakpoint_cond_big': [('fa.question-circle',), {'color': 'darkred'},], + 'debug': [('spyder.debug',), {'color': '#3775a9'}], + 'arrow-step-over': [('spyder.step-forward',), {'color': '#3775a9'}], + 'arrow-continue': [('spyder.continue',), {'color': '#3775a9'}], + 'arrow-step-in': [('spyder.step-into',), {'color': '#3775a9'}], + 'arrow-step-out': [('spyder.step-out',), {'color': '#3775a9'}], + 'stop_debug': [('fa.stop',), {'color': '#3775a9'}], + 'run': [('fa.play',), {'color': 'green'}], + 'run_settings': [('fa.wrench', 'fa.play'), {'options': [{'offset':(0.0, -0.1)}, {'offset': (0.2, 0.125), 'color': 'green', 'scale_factor': 0.8}]}], + 'run_again': [('fa.repeat', 'fa.play'), {'options': [{'offset':(0.0, -0.1)}, {'offset': (0.2, 0.125), 'color': 'green', 'scale_factor': 0.8}]}], + 'run_selection': [('spyder.run-selection',), {}], + 'run_cell': [('spyder.cell-code', 'spyder.cell-border', 'spyder.cell-play'), + {'options': [{'color': '#fff683'}, {}, {'color': 'green'}]}], + 'run_cell_advance': [('spyder.cell-code', 'spyder.cell-border', 'spyder.cell-play', 'spyder.cell-next'), + {'options': [{'color': '#fff683'}, {}, {'color': 'green'}, {'color': 'red'}]}], + 'todo_list': [('fa.th-list', 'fa.check'), {'options': [{'color': '#999999'}, {'offset': (0.0, 0.2), 'color': '#3775a9', 'color_disabled': '#748fa6'}]}], + 'wng_list': [('fa.th-list', 'fa.warning'), {'options': [{'color': '#999999'}, {'offset': (0.0, 0.2), 'scale_factor': 0.75, 'color': 'orange', 'color_disabled': '#face7e'}]}], + 'prev_wng': [('fa.arrow-left', 'fa.warning'), {'options': [{'color': '#999999'}, {'offset': (0.0, 0.2), 'scale_factor': 0.75, 'color': 'orange', 'color_disabled': '#face7e'}]}], + 'next_wng': [('fa.arrow-right', 'fa.warning'), {'options': [{'color': '999999'}, {'offset': (0.0, 0.2), 'scale_factor': 0.75, 'color': 'orange', 'color_disabled': '#face7e'}]}], + 'last_edit_location': [('fa.caret-up',), {}], + 'prev_cursor': [('fa.hand-o-left',), {}], + 'next_cursor': [('fa.hand-o-right',), {}], + 'comment': [('fa.comment',), {}], + 'indent': [('fa.indent',), {}], + 'unindent': [('fa.outdent',), {}], + 'gotoline': [('fa.sort-numeric-asc',), {}], + 'error': [('fa.times-circle',), {'color': 'darkred'}], + 'warning': [('fa.warning',), {'color': 'orange'}], + 'todo': [('fa.check',), {'color': '#3775a9'}], + 'ipython_console': [('spyder.ipython-logo-alt',), {}], + 'ipython_console_t': [('spyder.ipython-logo-alt',), {'color':'gray'}], + 'python': [('spyder.python-logo-up', 'spyder.python-logo-down'), {'options': [{'color': '#3775a9'}, {'color': '#ffd444'}]}], + 'python_t': [('spyder.python-logo',), {'color':'gray'}], + 'pythonpath': [('spyder.python-logo-up', 'spyder.python-logo-down'), {'options': [{'color': '#3775a9'}, {'color': '#ffd444'}]}], + 'terminated': [('fa.circle',), {}], + 'cmdprompt': [('fa.terminal',), {}], + 'cmdprompt_t': [('fa.terminal',), {'color':'gray'}], + 'console': [('spyder.python-logo-up', 'spyder.python-logo-down'), {'options': [{'color': '#3775a9'}, {'color': '#ffd444'}]}], + 'findf': [('fa.file-o', 'fa.search'), {'options': [{'scale_factor': 1.0}, {'scale_factor': 0.6}]}], + 'history24': [('fa.history',), {}], + 'history': [('fa.history',), {}], + 'help': [('fa.question-circle',), {}], + 'lock': [('fa.lock',), {}], + 'lock_open': [('fa.unlock-alt',), {}], + 'outline_explorer': [('spyder.treeview',), {}], + 'project_expanded': [('fa.plus',), {}], + 'dictedit': [('fa.th-list',), {}], + 'previous': [('fa.arrow-left',), {}], + 'next': [('fa.arrow-right',), {}], + 'set_workdir': [('fa.check',), {}], + 'up': [('fa.arrow-up',), {}], + 'down': [('fa.arrow-down',), {}], + 'filesaveas2': [('fa.save', 'fa.close'), {'options': [{'scale_factor': 0.8, 'offset': (-0.1, -0.1)}, {'offset': (0.2, 0.2)}]}], # save_session_action + 'spyder': [('spyder.spyder-logo-background', 'spyder.spyder-logo-web', 'spyder.spyder-logo-snake'), {'options': [{'color': '#414141'}, {'color': '#fafafa'}, {'color': '#ee0000'}]}], + 'find': [('fa.search',), {}], + 'findnext': [('fa.search', 'fa.long-arrow-down'), {'options':[{'scale_factor': 0.6, 'offset': (0.3, 0.0)}, {'offset': (-0.3, 0.0)}]}], + 'findprevious': [('fa.search', 'fa.long-arrow-up'), {'options':[{'scale_factor': 0.6, 'offset': (0.3, 0.0)}, {'offset': (-0.3, 0.0)}]}], + 'replace': [('fa.exchange',), {}], + 'undo': [('fa.undo',), {}], + 'redo': [('fa.repeat',), {}], + 'restart': [('fa.repeat',), {'çolor': '#3775a9'}], + 'editcopy': [('fa.copy',), {}], + 'editcut': [('fa.scissors',), {}], + 'editpaste': [('fa.clipboard',), {}], + 'editdelete': [('fa.eraser',), {}], + 'editclear': [('fa.times',), {}], + 'selectall': [('spyder.text-select-all',), {}], + 'pythonpath_mgr': [('spyder.python-logo-up', 'spyder.python-logo-down'), {'options': [{'color': '#3775a9'}, {'color': '#ffd444'}]}], + 'exit': [('fa.power-off',), {'color': 'darkred'}], + 'advanced': [('fa.gear',), {}], + 'bug': [('fa.bug',), {}], + 'maximize': [('spyder.maximize-pane',), {}], + 'unmaximize': [('spyder.minimize-pane',), {}], + 'window_nofullscreen': [('spyder.inward',), {}], + 'window_fullscreen': [('fa.arrows-alt',), {}], + 'MessageBoxWarning': [('fa.warning',), {}], + 'arredit': [('fa.table',), {}], + 'zoom_out': [('fa.search-minus',), {}], + 'zoom_in': [('fa.search-plus',), {}], + 'home': [('fa.home',), {}], + 'find': [('fa.search',), {}], + 'plot': [('fa.line-chart',), {}], + 'hist': [('fa.bar-chart',), {}], + 'imshow': [('fa.image',), {}], + 'insert': [('fa.sign-in',), {}], + 'rename': [('fa.pencil',), {}], + 'edit_add': [('fa.plus',), {}], + 'edit_remove': [('fa.minus',), {}], + 'browse_tab': [('fa.folder-o',), {}], + 'filelist': [('fa.list',), {}], + 'newwindow': [('spyder.window',), {}], + 'versplit': [('spyder.rows',), {}], + 'horsplit': [('fa.columns',), {}], + 'close_panel': [('fa.close',), {}], + 'class': [('spyder.circle-letter-c',), {'color':'#3775a9'}], + 'private2': [('spyder.circle-underscore',), {'color':'#e69c9c'}], + 'private1': [('spyder.circle-underscore',), {'color':'#e69c9c'}], + 'method': [('spyder.circle-letter-m',), {'color':'#7ea67e'}], + 'function': [('spyder.circle-letter-f',), {'color':'orange'}], + 'blockcomment': [('spyder.circle-hash',), {'color':'grey'}], + 'cell': [('spyder.circle-percent',), {'color':'red'}], + 'fromcursor': [('fa.hand-o-right',), {}], + 'filter': [('fa.filter',), {}], + 'folder_new': [('fa.folder-o', 'fa.plus'), {'options': [{}, {'scale_factor': 0.5, 'offset': (0.0, 0.1)}]}], + 'package_new': [('fa.folder-o', 'spyder.python-logo'), {'options': [{'offset': (0.0, -0.125)}, {'offset': (0.0, 0.125)}]}], + 'vcs_commit': [('fa.check',), {'color': 'green'}], + 'vcs_browse': [('fa.search',), {'color': 'green'}], + 'kill': [('fa.warning',), {}], + 'reload': [('fa.repeat',), {}], + 'auto_reload': [('fa.repeat', 'fa.clock-o'), {'options': [{'scale_factor': 0.75, 'offset': (-0.1, -0.1)}, {'scale_factor': 0.5, 'offset': (0.25, 0.25)}]}], + 'fileimport': [('fa.download',), {}], + 'environ': [('fa.th-list',), {}], + 'options_less': [('fa.minus-square',), {}], + 'options_more': [('fa.plus-square',), {}], + 'ArrowDown': [('fa.arrow-circle-down',), {}], + 'ArrowUp': [('fa.arrow-circle-up',), {}], + 'ArrowBack': [('fa.arrow-circle-left',), {}], + 'ArrowForward': [('fa.arrow-circle-right',), {}], + 'DialogApplyButton': [('fa.check',), {}], + 'DialogCloseButton': [('fa.close',), {}], + 'DirClosedIcon': [('fa.folder-o',), {}], + 'DialogHelpButton': [('fa.life-ring',), {'color': 'darkred'}], + 'MessageBoxInformation': [('fa.info',), {'color': '3775a9'}], + 'DirOpenIcon': [('fa.folder-open',), {}], + 'FileIcon': [('fa.file-o',), {}], + 'project': [('fa.folder-open-o',), {}], + 'DriveHDIcon': [('fa.hdd-o',), {}], + 'arrow': [('fa.arrow-right',), {}], + 'collapse': [('spyder.inward',), {}], + 'expand': [('fa.arrows-alt',), {}], + 'restore': [('fa.level-up',), {}], + 'collapse_selection': [('fa.minus-square-o',), {}], + 'expand_selection': [('fa.plus-square-o',), {}], + 'copywop': [('fa.terminal',), {}], + 'editpaste': [('fa.paste',), {}], + 'editcopy': [('fa.copy',), {}], + 'edit': [('fa.edit',), {}], + 'convention': [('spyder.circle-letter-c',), {'color':'#3775a9'}], + 'refactor': [('spyder.circle-letter-r',), {'color':'#3775a9'}], + '2uparrow': [('fa.angle-double-up',), {}], + '1uparrow': [('fa.angle-up',), {}], + '2downarrow': [('fa.angle-double-down',), {}], + '1downarrow': [('fa.angle-down',), {}], + 'attribute': [('spyder.circle-letter-a',), {'color': 'magenta'}], + 'module': [('spyder.circle-letter-m',), {'color': '#daa520'}], + 'no_match': [('fa.circle',), {'color': 'gray'}], + 'no_match': [('fa.circle',), {'color': 'gray'}], + # --- Third party plugins ------------------------------------------------ + 'profiler': [('fa.clock-o',), {}], + 'pylint': [('fa.search', 'fa.check'), {'options': [{}, {'offset': (0.125, 0.125), 'color': 'orange'}]}], + 'condapackages': [('fa.archive',), {}], + 'spyder.example': [('fa.eye',), {}], + 'spyder.autopep8': [('fa.eye',), {}], + 'spyder.memory_profiler': [('fa.eye',), {}], + 'spyder.line_profiler': [('fa.eye',), {}], +} + + +def get_std_icon(name, size=None): + """Get standard platform icon + Call 'show_std_icons()' for details""" + if not name.startswith('SP_'): + name = 'SP_' + name + icon = QWidget().style().standardIcon(getattr(QStyle, name)) + if size is None: + return icon + else: + return QIcon(icon.pixmap(size, size)) + + +def get_icon(name, default=None, resample=False): + """Return image inside a QIcon object. + + default: default image name or icon + resample: if True, manually resample icon pixmaps for usual sizes + (16, 24, 32, 48, 96, 128, 256). This is recommended for QMainWindow icons + created from SVG images on non-Windows platforms due to a Qt bug (see + Issue 1314). + """ + + icon_path = get_image_path(name, default=None) + if icon_path is not None: + icon = QIcon(icon_path) + elif isinstance(default, QIcon): + icon = default + elif default is None: + try: + icon = get_std_icon(name[:-4]) + except AttributeError: + icon = QIcon(get_image_path(name, default)) + else: + icon = QIcon(get_image_path(name, default)) + if resample: + icon0 = QIcon() + for size in (16, 24, 32, 48, 96, 128, 256, 512): + icon0.addPixmap(icon.pixmap(size, size)) + return icon0 + else: + return icon + + +def icon(name, resample=False, icon_path=None): + theme = CONF.get('main', 'icon_theme') + if theme == 'spyder 3': + if not _resource['loaded']: + qta.load_font('spyder', 'spyder.ttf', 'spyder-charmap.json', + directory=_resource['directory']) + _resource['loaded'] = True + args, kwargs = _qtaargs[name] + return qta.icon(*args, **kwargs) + elif theme == 'spyder 2': + icon = get_icon(name + '.png', resample=resample) + if icon_path: + icon_path = osp.join(icon_path, name + '.png') + if osp.isfile(icon_path): + icon = QIcon(icon_path) + return icon if icon is not None else QIcon() diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/utils/__init__.py spyder-3.0.2+dfsg1/spyder/utils/__init__.py --- spyder-2.3.8+dfsg1/spyder/utils/__init__.py 2015-08-24 21:09:44.000000000 +0200 +++ spyder-3.0.2+dfsg1/spyder/utils/__init__.py 2016-10-25 02:05:23.000000000 +0200 @@ -1,12 +1,12 @@ # -*- coding: utf-8 -*- # -# Copyright © 2009-2010 Pierre Raybaut +# Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) +# (see spyder/__init__.py for details) """ -spyderlib.utils -=============== +spyder.utils +============ Spyder utilities """ diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/utils/inputhooks.py spyder-3.0.2+dfsg1/spyder/utils/inputhooks.py --- spyder-2.3.8+dfsg1/spyder/utils/inputhooks.py 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/utils/inputhooks.py 2016-10-25 02:05:23.000000000 +0200 @@ -0,0 +1,158 @@ +# -*- coding: utf-8 -*- +""" +Inputhook management for GUI event loop integration + +Copyright (C) The IPython Development Team +Distributed under the terms of the modified BSD license +""" + +# Stdlib imports +import ctypes +import os +import sys + +QT_API = os.environ["QT_API"] + +# Qt imports +if QT_API == 'pyqt5': + from PyQt5 import QtCore +elif QT_API == 'pyqt': + from PyQt4 import QtCore +elif QT_API == 'pyside': + from PySide import QtCore + + +#----------------------------------------------------------------------------- +# Utilities +#----------------------------------------------------------------------------- +def _stdin_ready_posix(): + """Return True if there's something to read on stdin (posix version).""" + infds, outfds, erfds = select.select([sys.stdin],[],[],0) + return bool(infds) + +def _stdin_ready_nt(): + """Return True if there's something to read on stdin (nt version).""" + return msvcrt.kbhit() + +def _stdin_ready_other(): + """Return True, assuming there's something to read on stdin.""" + return True + + +def _ignore_CTRL_C_posix(): + """Ignore CTRL+C (SIGINT).""" + signal.signal(signal.SIGINT, signal.SIG_IGN) + +def _allow_CTRL_C_posix(): + """Take CTRL+C into account (SIGINT).""" + signal.signal(signal.SIGINT, signal.default_int_handler) + +def _ignore_CTRL_C_other(): + """Ignore CTRL+C (not implemented).""" + pass + +def _allow_CTRL_C_other(): + """Take CTRL+C into account (not implemented).""" + pass + + +if os.name == 'posix': + import select + import signal + stdin_ready = _stdin_ready_posix + ignore_CTRL_C = _ignore_CTRL_C_posix + allow_CTRL_C = _allow_CTRL_C_posix +elif os.name == 'nt': + import msvcrt + stdin_ready = _stdin_ready_nt + ignore_CTRL_C = _ignore_CTRL_C_other + allow_CTRL_C = _allow_CTRL_C_other +else: + stdin_ready = _stdin_ready_other + ignore_CTRL_C = _ignore_CTRL_C_other + allow_CTRL_C = _allow_CTRL_C_other + + +def clear_inputhook(): + """Set PyOS_InputHook to NULL and return the previous one""" + pyos_inputhook_ptr = ctypes.c_void_p.in_dll(ctypes.pythonapi, + "PyOS_InputHook") + pyos_inputhook_ptr.value = ctypes.c_void_p(None).value + allow_CTRL_C() + +def get_pyos_inputhook(): + """Return the current PyOS_InputHook as a ctypes.c_void_p.""" + return ctypes.c_void_p.in_dll(ctypes.pythonapi, "PyOS_InputHook") + +def set_pyft_callback(callback): + callback = ctypes.PYFUNCTYPE(ctypes.c_int)(callback) + return callback + +def remove_pyqt_inputhook(): + if QT_API == 'pyqt' or QT_API == 'pyqt5': + QtCore.pyqtRemoveInputHook() + else: + pass + + +#------------------------------------------------------------------------------ +# Input hooks +#------------------------------------------------------------------------------ +def qt4(): + """PyOS_InputHook python hook for Qt4. + + Process pending Qt events and if there's no pending keyboard + input, spend a short slice of time (50ms) running the Qt event + loop. + + As a Python ctypes callback can't raise an exception, we catch + the KeyboardInterrupt and temporarily deactivate the hook, + which will let a *second* CTRL+C be processed normally and go + back to a clean prompt line. + """ + try: + allow_CTRL_C() + app = QtCore.QCoreApplication.instance() + if not app: + return 0 + app.processEvents(QtCore.QEventLoop.AllEvents, 300) + if not stdin_ready(): + # Generally a program would run QCoreApplication::exec() + # from main() to enter and process the Qt event loop until + # quit() or exit() is called and the program terminates. + # + # For our input hook integration, we need to repeatedly + # enter and process the Qt event loop for only a short + # amount of time (say 50ms) to ensure that Python stays + # responsive to other user inputs. + # + # A naive approach would be to repeatedly call + # QCoreApplication::exec(), using a timer to quit after a + # short amount of time. Unfortunately, QCoreApplication + # emits an aboutToQuit signal before stopping, which has + # the undesirable effect of closing all modal windows. + # + # To work around this problem, we instead create a + # QEventLoop and call QEventLoop::exec(). Other than + # setting some state variables which do not seem to be + # used anywhere, the only thing QCoreApplication adds is + # the aboutToQuit signal which is precisely what we are + # trying to avoid. + timer = QtCore.QTimer() + event_loop = QtCore.QEventLoop() + timer.timeout.connect(event_loop.quit) + while not stdin_ready(): + timer.start(50) + event_loop.exec_() + timer.stop() + except KeyboardInterrupt: + print("\nKeyboardInterrupt - Press Enter for new prompt") + except: # NO exceptions are allowed to escape from a ctypes callback + ignore_CTRL_C() + from traceback import print_exc + print_exc() + print("Got exception from inputhook, unregistering.") + clear_inputhook() + finally: + allow_CTRL_C() + return 0 diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/utils/inspector/conf.py spyder-3.0.2+dfsg1/spyder/utils/inspector/conf.py --- spyder-2.3.8+dfsg1/spyder/utils/inspector/conf.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/utils/inspector/conf.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,116 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2009 Tim Dumol -# Copyright (C) 2013 The Spyder Development Team -# Distributed under the terms of the BSD License - -"""Sphinx conf file for the object inspector rich text mode""" - -# 3rd party imports -from sphinx import __version__ as sphinx_version - -# Local imports -from spyderlib.config import CONF -from spyderlib.py3compat import u - -#============================================================================== -# General configuration -#============================================================================== - -# If your extensions are in another directory, add it here. If the directory -# is relative to the documentation root, use os.path.abspath to make it -# absolute, like shown here. -#sys.path.append(os.path.abspath('.')) - -# Add any Sphinx extension module names here, as strings. They can be extensions -# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. - -# We need jsmath to get pretty plain-text latex in docstrings -math = CONF.get('inspector', 'math', '') - -if sphinx_version < "1.1" or not math: - extensions = ['sphinx.ext.jsmath'] -else: - extensions = ['sphinx.ext.mathjax'] - -# For scipy and matplotlib docstrings, which need this extension to -# be rendered correctly (see Issue 1138) -extensions.append('sphinx.ext.autosummary') - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['templates'] - -# MathJax load path (doesn't have effect for sphinx 1.0-) -mathjax_path = 'MathJax/MathJax.js' - -# JsMath load path (doesn't have effect for sphinx 1.1+) -jsmath_path = 'easy/load.js' - -# The suffix of source filenames. -source_suffix = '.rst' - -# The master toctree document. -master_doc = 'docstring' - -# General information about the project. -project = u("Object Inspector") -copyright = u('2009--2013, The Spyder Development Team') - -# List of directories, relative to source directory, that shouldn't be searched -# for source files. -exclude_trees = ['.build'] - -# The reST default role (used for this markup: `text`) to use for all documents. -# -# TODO: This role has to be set on a per project basis, i.e. numpy, sympy, -# mpmath, etc, use different default_role's which give different rendered -# docstrings. Setting this to None until it's solved. -default_role = 'None' - -# If true, '()' will be appended to :func: etc. cross-reference text. -add_function_parentheses = True - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' - -#============================================================================== -# Options for HTML output -#============================================================================== - -# The style sheet to use for HTML and HTML Help pages. A file of that name -# must exist either in Sphinx' static/ path, or in one of the custom paths -# given in html_static_path. -html_style = 'default.css' - -# The name for this set of Sphinx documents. If None, it defaults to -# " v documentation". -html_title = None - -# A shorter title for the navigation bar. Default is the same as html_title. -html_short_title = None - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['static'] - -# A dictionary of values to pass into the template engine’s context for all -# pages -html_context = {} - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -#html_use_smartypants = True - -# If false, no module index is generated. -html_use_modindex = False - -# If false, no index is generated. -html_use_index = False - -# If true, the index is split into individual pages for each letter. -html_split_index = False - -# If true, the reST sources are included in the HTML build as _sources/. -html_copy_source = False - diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/utils/inspector/__init__.py spyder-3.0.2+dfsg1/spyder/utils/inspector/__init__.py --- spyder-2.3.8+dfsg1/spyder/utils/inspector/__init__.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/utils/inspector/__init__.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,16 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright © 2012 Spyder Development team -# Licensed under the terms of the MIT or BSD Licenses -# (See every file for its license) - -""" -spyderlib.utils.inspector -======================== - -Configuration files for the object inspector rich text mode -""" - -import sys -from spyderlib.baseconfig import get_module_source_path -sys.path.insert(0, get_module_source_path(__name__)) \ Kein Zeilenumbruch am Dateiende. diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/utils/inspector/js/collapse_sections.js spyder-3.0.2+dfsg1/spyder/utils/inspector/js/collapse_sections.js --- spyder-2.3.8+dfsg1/spyder/utils/inspector/js/collapse_sections.js 2015-08-24 21:09:44.000000000 +0200 +++ spyder-3.0.2+dfsg1/spyder/utils/inspector/js/collapse_sections.js 1970-01-01 01:00:00.000000000 +0100 @@ -1,65 +0,0 @@ -//---------------------------------------------------------------------------- -// Toggleable sections -// -// Added expand/collapse functionality to RST sections. -// Code from the Cloud Sphinx theme -// -// Copyright 2011-2012 by Assurance Technologies -// -// Distributed under the terms of the BSD License -//---------------------------------------------------------------------------- - -//============================================================================ -// On document ready -//============================================================================ - -$(document).ready(function (){ - function init(){ - // get header & section, and add static classes - var header = $(this); - var section = header.parent(); - header.addClass("html-toggle-button"); - - // helper to test if url hash is within this section - function contains_hash(){ - var hash = document.location.hash; - return hash && (section[0].id == hash.substr(1) || - section.find(hash.replace(/\./g,"\\.")).length>0); - } - - // helper to control toggle state - function set_state(expanded){ - if(expanded){ - section.addClass("expanded").removeClass("collapsed"); - section.children().show(); - }else{ - section.addClass("collapsed").removeClass("expanded"); - section.children().hide(); - section.children("span:first-child:empty").show(); /* for :ref: span tag */ - header.show(); - } - } - - // initialize state - set_state(section.hasClass("expanded") || contains_hash()); - - // bind toggle callback - header.click(function (){ - section.children().next().slideToggle(300); - set_state(!section.hasClass("expanded")); - $(window).trigger('cloud-section-toggled', section[0]); - }); - - // open section if user jumps to it from w/in page - $(window).bind("hashchange", function () { - if(contains_hash()) { - var link = document.location.hash; - $(link).parents().each(set_state, [true]); - set_state(true); - $('html, body').animate({ scrollTop: $(link).offset().top }, 'fast'); - } - }); - } - - $(".section > h2, .section > h3, .section > h4").each(init); -}); diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/utils/inspector/js/copy_button.js spyder-3.0.2+dfsg1/spyder/utils/inspector/js/copy_button.js --- spyder-2.3.8+dfsg1/spyder/utils/inspector/js/copy_button.js 2015-08-24 21:09:44.000000000 +0200 +++ spyder-3.0.2+dfsg1/spyder/utils/inspector/js/copy_button.js 1970-01-01 01:00:00.000000000 +0100 @@ -1,68 +0,0 @@ -//---------------------------------------------------------------------------- -// Copy Button -// -// Add a [>>>] button on the top-right corner of code samples to hide -// the >>> and ... prompts and the output and thus make the code -// copyable. -// -// Taken from http://docs.python.org/_static/copybutton.js -//---------------------------------------------------------------------------- - -//============================================================================ -// On document ready -//============================================================================ - -$(document).ready(function() { - var div = $('.highlight-python .highlight,' + - '.highlight-python3 .highlight') - var pre = div.find('pre'); - - // get the styles from the current theme - pre.parent().parent().css('position', 'relative'); - var hide_text = 'Hide the prompts and output'; - var show_text = 'Show the prompts and output'; - var border_width = pre.css('border-top-width'); - var border_style = pre.css('border-top-style'); - var border_color = pre.css('border-top-color'); - var button_styles = { - 'cursor':'pointer', 'position': 'absolute', 'top': '0', 'right': '0', - 'border-color': border_color, 'border-style': border_style, - 'border-width': border_width, 'color': border_color, 'text-size': '75%', - 'font-family': 'monospace', 'padding-left': '0.2em', 'padding-right': '0.2em', - 'margin-right': '10px', 'border-top-right-radius': '4px' - } - - // create and add the button to all the code blocks that contain >>> - div.each(function(index) { - var jthis = $(this); - if (jthis.find('.gp').length > 0) { - var button = $('>>>'); - button.css(button_styles) - button.attr('title', hide_text); - jthis.prepend(button); - } - // tracebacks (.gt) contain bare text elements that need to be - // wrapped in a span to work with .nextUntil() (see later) - jthis.find('pre:has(.gt)').contents().filter(function() { - return ((this.nodeType == 3) && (this.data.trim().length > 0)); - }).wrap(''); - }); - - // define the behavior of the button when it's clicked - $('.copybutton').toggle( - function() { - var button = $(this); - button.parent().find('.go, .gp, .gt').hide(); - button.next('pre').find('.gt').nextUntil('.gp, .go').css('visibility', 'hidden'); - button.css('text-decoration', 'line-through'); - button.attr('title', show_text); - }, - function() { - var button = $(this); - button.parent().find('.go, .gp, .gt').show(); - button.next('pre').find('.gt').nextUntil('.gp, .go').css('visibility', 'visible'); - button.css('text-decoration', 'none'); - button.attr('title', hide_text); - }); -}); - diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/utils/inspector/js/fix_image_paths.js spyder-3.0.2+dfsg1/spyder/utils/inspector/js/fix_image_paths.js --- spyder-2.3.8+dfsg1/spyder/utils/inspector/js/fix_image_paths.js 2015-08-24 21:09:44.000000000 +0200 +++ spyder-3.0.2+dfsg1/spyder/utils/inspector/js/fix_image_paths.js 1970-01-01 01:00:00.000000000 +0100 @@ -1,19 +0,0 @@ -//---------------------------------------------------------------------------- -// Set absolute path for images -// -// Copyright 2014 by The Spyder development team -// -// Distributed under the terms of the MIT License -//---------------------------------------------------------------------------- - -//============================================================================ -// On document ready -//============================================================================ - -$(document).ready(function () { - $('img').attr('src', function(index, attr){ - var path = attr.split('/') - var img_name = path.reverse()[0] - return '{{img_path}}' + '/' + img_name - }); -}); diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/utils/inspector/js/math_config.js spyder-3.0.2+dfsg1/spyder/utils/inspector/js/math_config.js --- spyder-2.3.8+dfsg1/spyder/utils/inspector/js/math_config.js 2015-08-24 21:09:44.000000000 +0200 +++ spyder-3.0.2+dfsg1/spyder/utils/inspector/js/math_config.js 1970-01-01 01:00:00.000000000 +0100 @@ -1,74 +0,0 @@ -//---------------------------------------------------------------------------- -// Math configuration options and hacks -// -// Copyright (C) 2012 - The Spyder Team -// -// Distributed under the terms of the MIT License. -//---------------------------------------------------------------------------- - -//============================================================================ -// On document ready -//============================================================================ - -{% if right_sphinx_version and math_on %} - -$(document).ready(function () { - - // MathJax config - // -------------- - MathJax.Hub.Config({ - // We are using SVG instead of HTML-CSS because the last one gives - // troubles on QtWebkit. See this thread: - // https://groups.google.com/forum/?fromgroups#!topic/mathjax-users/HKA2lNqv-OQ - jax: ["input/TeX", "output/SVG"], - - // Menu options are not working. It would be useful to have 'Show TeX - // commands', but it opens an external browser pointing to css_path. - // I don't know why that's happening - showMathMenu: false, - messageStyle: "none", - "SVG": { - blacker: 1 - }, - - {% if platform == 'win32' %} - // Change math preview size so that it doesn't look too big while - // redendered - styles: { - ".MathJax_Preview": { - color: "#888", - "font-size": "55%" - } - } - {% endif %} - }); - - // MathJax Hooks - // ------------- - // Put here any code that needs to be evaluated after MathJax has been - // fully loaded - MathJax.Hub.Register.StartupHook("End", function () { - // Eliminate unnecessary margin-bottom for inline math - $('span.math svg').css('margin-bottom', '0px'); - }); - - {% if platform == 'win32' %} - // Windows fix - // ----------- - // Increase font size of math elements because they appear too small - // compared to the surrounding text. - // Use this hack because MathJax 'scale' option seems to not be working - // for SVG. - $('.math').css("color", "transparent"); - $('.math').css("fontSize", "213%"); - {% endif %} -}); - -{% else %} - -$(document).ready(function () { - // Show math in monospace - $('.math').css('font-family', 'monospace'); -}); - -{% endif %} diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/utils/inspector/js/move_outline.js spyder-3.0.2+dfsg1/spyder/utils/inspector/js/move_outline.js --- spyder-2.3.8+dfsg1/spyder/utils/inspector/js/move_outline.js 2015-08-24 21:09:46.000000000 +0200 +++ spyder-3.0.2+dfsg1/spyder/utils/inspector/js/move_outline.js 1970-01-01 01:00:00.000000000 +0100 @@ -1,16 +0,0 @@ -//---------------------------------------------------------------------------- -// Move Outline section to be the first one -// -// Copyright 2014 by The Spyder development team -// -// Distributed under the terms of the MIT License -//---------------------------------------------------------------------------- - -//============================================================================ -// On document ready -//============================================================================ - -$(document).ready(function (){ - var first_section_id = $(".section")[0].id; - $("#outline").insertBefore("#" + first_section_id); -}); diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/utils/inspector/js/utils.js spyder-3.0.2+dfsg1/spyder/utils/inspector/js/utils.js --- spyder-2.3.8+dfsg1/spyder/utils/inspector/js/utils.js 2015-08-24 21:09:46.000000000 +0200 +++ spyder-3.0.2+dfsg1/spyder/utils/inspector/js/utils.js 1970-01-01 01:00:00.000000000 +0100 @@ -1,34 +0,0 @@ -//---------------------------------------------------------------------------- -// Several utility functions to modify docstring webpages while they are -// rendered -// -// Copyright (C) 2012 - The Spyder Team -// -// Distributed under the terms of the MIT License. -//---------------------------------------------------------------------------- - -//============================================================================ -// On document ready -//============================================================================ - -$(document).ready(function () { - // Remove anchor header links. - // They're used by Sphinx to create crossrefs, so we don't need them - $('a.headerlink').remove(); - - // If the first child in the docstring div is a section, change its class - // to title. This means that the docstring has a real title and we need - // to use it. - // This is really useful to show module docstrings. - var first_doc_child = $('div.docstring').children(':first-child'); - if( first_doc_child.is('div.section') && $('div.title').length == 0 ) { - first_doc_child.removeClass('section').addClass('title'); - }; - - // Change docstring headers from h1 to h2 - // It can only be an h1 and that's the page title - // Taken from http://forum.jquery.com/topic/how-to-replace-h1-h2 - $('div.docstring').find('div.section h1').replaceWith(function () { - return '

    ' + $(this).text() + '

    '; - }); -}); diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/utils/inspector/sphinxify.py spyder-3.0.2+dfsg1/spyder/utils/inspector/sphinxify.py --- spyder-2.3.8+dfsg1/spyder/utils/inspector/sphinxify.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/utils/inspector/sphinxify.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,253 +0,0 @@ -# -*- coding: utf-8 -* - -""" -Process docstrings with Sphinx - -AUTHORS: -- Tim Joseph Dumol (2009-09-29): initial version -- The Spyder Development Team: Several changes to make it work with Spyder - -Copyright (C) 2009 Tim Dumol -Copyright (C) 2013 The Spyder Development Team -Distributed under the terms of the BSD License - -Taken from the Sage project (www.sagemath.org). -See here for the original version: -www.sagemath.org/doc/reference/sagenb/misc/sphinxify.html -""" - -# Stdlib imports -import codecs -import os -import os.path as osp -import shutil -import sys -from tempfile import mkdtemp - -# 3rd party imports -from docutils.utils import SystemMessage as SystemMessage -from jinja2 import Environment, FileSystemLoader -import sphinx -from sphinx.application import Sphinx - -# Local imports -from spyderlib.baseconfig import (_, get_module_data_path, - get_module_source_path) -from spyderlib.utils import encoding - - -#----------------------------------------------------------------------------- -# Globals and constants -#----------------------------------------------------------------------------- - -# Note: we do not use __file__ because it won't be working in the stand-alone -# version of Spyder (i.e. the py2exe or cx_Freeze build) -CONFDIR_PATH = get_module_source_path('spyderlib.utils.inspector') -CSS_PATH = osp.join(CONFDIR_PATH, 'static', 'css') -JS_PATH = osp.join(CONFDIR_PATH, 'js') - -# To let Debian packagers redefine the MathJax and JQuery locations so they can -# use their own packages for them. See Issue 1230, comment #7. -MATHJAX_PATH = get_module_data_path('spyderlib', - relpath=osp.join('utils', 'inspector', - JS_PATH, 'mathjax'), - attr_name='MATHJAXPATH') - -JQUERY_PATH = get_module_data_path('spyderlib', - relpath=osp.join('utils', 'inspector', - JS_PATH), - attr_name='JQUERYPATH') - -#----------------------------------------------------------------------------- -# Utility functions -#----------------------------------------------------------------------------- - -def is_sphinx_markup(docstring): - """Returns whether a string contains Sphinx-style ReST markup.""" - # this could be made much more clever - return ("`" in docstring or "::" in docstring) - - -def warning(message): - """Print a warning message on the rich text view""" - env = Environment() - env.loader = FileSystemLoader(osp.join(CONFDIR_PATH, 'templates')) - warning = env.get_template("warning.html") - return warning.render(css_path=CSS_PATH, text=message) - - -def usage(title, message, tutorial_message, tutorial): - """Print a usage message on the rich text view""" - env = Environment() - env.loader = FileSystemLoader(osp.join(CONFDIR_PATH, 'templates')) - usage = env.get_template("usage.html") - return usage.render(css_path=CSS_PATH, title=title, intro_message=message, - tutorial_message=tutorial_message, tutorial=tutorial) - - -def generate_context(name='', argspec='', note='', math=False, collapse=False, - img_path=''): - """ - Generate the html_context dictionary for our Sphinx conf file. - - This is a set of variables to be passed to the Jinja template engine and - that are used to control how the webpage is rendered in connection with - Sphinx - - Parameters - ---------- - name : str - Object's name. - note : str - A note describing what type has the function or method being - introspected - argspec : str - Argspec of the the function or method being introspected - math : bool - Turn on/off Latex rendering on the OI. If False, Latex will be shown in - plain text. - collapse : bool - Collapse sections - - Returns - ------- - A dict of strings to be used by Jinja to generate the webpage - """ - - context = \ - { - # Arg dependent variables - 'math_on': 'true' if math else '', - 'name': name, - 'argspec': argspec, - 'note': note, - 'collapse': collapse, - 'img_path': img_path, - - # Static variables - 'css_path': CSS_PATH, - 'js_path': JS_PATH, - 'jquery_path': JQUERY_PATH, - 'mathjax_path': MATHJAX_PATH, - 'right_sphinx_version': '' if sphinx.__version__ < "1.1" else 'true', - 'platform': sys.platform - } - - return context - - -def sphinxify(docstring, context, buildername='html'): - """ - Runs Sphinx on a docstring and outputs the processed documentation. - - Parameters - ---------- - docstring : str - a ReST-formatted docstring - - context : dict - Variables to be passed to the layout template to control how its - rendered (through the Sphinx variable *html_context*). - - buildername: str - It can be either `html` or `text`. - - Returns - ------- - An Sphinx-processed string, in either HTML or plain text format, depending - on the value of `buildername` - """ - - srcdir = mkdtemp() - srcdir = encoding.to_unicode_from_fs(srcdir) - - base_name = osp.join(srcdir, 'docstring') - rst_name = base_name + '.rst' - - if buildername == 'html': - suffix = '.html' - else: - suffix = '.txt' - output_name = base_name + suffix - - # This is needed so users can type \\ on latex eqnarray envs inside raw - # docstrings - if context['right_sphinx_version'] and context['math_on']: - docstring = docstring.replace('\\\\', '\\\\\\\\') - - # Add a class to several characters on the argspec. This way we can - # highlight them using css, in a similar way to what IPython does. - argspec = context['argspec'] - for char in ['=', ',', '(', ')', '*', '**']: - argspec = argspec.replace(char, - '' + char + '') - context['argspec'] = argspec - - doc_file = codecs.open(rst_name, 'w', encoding='utf-8') - doc_file.write(docstring) - doc_file.close() - - temp_confdir = False - if temp_confdir: - # TODO: This may be inefficient. Find a faster way to do it. - confdir = mkdtemp() - confdir = encoding.to_unicode_from_fs(confdir) - generate_configuration(confdir) - else: - confdir = osp.join(get_module_source_path('spyderlib.utils.inspector')) - - confoverrides = {'html_context': context} - - doctreedir = osp.join(srcdir, 'doctrees') - - sphinx_app = Sphinx(srcdir, confdir, srcdir, doctreedir, buildername, - confoverrides, status=None, warning=None, - freshenv=True, warningiserror=False, tags=None) - try: - sphinx_app.build(None, [rst_name]) - except SystemMessage: - output = _("It was not possible to generate rich text help for this " - "object.
    " - "Please see it in plain text.") - return warning(output) - - # TODO: Investigate if this is necessary/important for us - if osp.exists(output_name): - output = codecs.open(output_name, 'r', encoding='utf-8').read() - output = output.replace('
    ', '
    ')
    -    else:
    -        output = _("It was not possible to generate rich text help for this "
    -                    "object.
    " - "Please see it in plain text.") - return warning(output) - - if temp_confdir: - shutil.rmtree(confdir, ignore_errors=True) - shutil.rmtree(srcdir, ignore_errors=True) - - return output - - -def generate_configuration(directory): - """ - Generates a Sphinx configuration in `directory`. - - Parameters - ---------- - directory : str - Base directory to use - """ - - # conf.py file for Sphinx - conf = osp.join(get_module_source_path('spyderlib.utils.inspector'), - 'conf.py') - - # Docstring layout page (in Jinja): - layout = osp.join(osp.join(CONFDIR_PATH, 'templates'), 'layout.html') - - os.makedirs(osp.join(directory, 'templates')) - os.makedirs(osp.join(directory, 'static')) - shutil.copy(conf, directory) - shutil.copy(layout, osp.join(directory, 'templates')) - open(osp.join(directory, '__init__.py'), 'w').write('') - open(osp.join(directory, 'static', 'empty'), 'w').write('') diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/utils/inspector/static/css/default.css spyder-3.0.2+dfsg1/spyder/utils/inspector/static/css/default.css --- spyder-2.3.8+dfsg1/spyder/utils/inspector/static/css/default.css 2015-08-24 21:09:46.000000000 +0200 +++ spyder-3.0.2+dfsg1/spyder/utils/inspector/static/css/default.css 1970-01-01 01:00:00.000000000 +0100 @@ -1,415 +0,0 @@ -body { - background-color: white; - color: rgb(51, 51, 51); - margin: 0px 25px 15px 25px; -} - - -/* --- Title style --- */ -div.title h1 { - font-size: 180%; - font-family: 'Trebuchet MS', sans-serif; - background-color: #6487DC; - background-image: -webkit-gradient( - linear, - 0 0, - 0 100%, - from(#54b4eb), - color-stop(60%, #2fa4e7), - to(#1d9ce5) - ); - text-shadow: 0px 1px 1px rgba(0, 0, 0, 0.2); - font-weight: normal; - padding: 6px 0px 6px 20px; - margin: 0px -25px; - color: #FFFFFF; -} - -/* - * The next two styles are needed to - * modify the anchors present on the - * title of pages like scipy.stats or - * scipy.io - */ -div.title h1 a { - color: transparent; - cursor: default; -} - -div.title h1 tt { - font-size: 95%; - background-color: transparent; - color: #FFFFFF; -} - - -/* --- Metadata style --- */ -div.metadata { - margin-top: 10px; - margin-bottom: 15px; - margin-right: 1px; - padding: 1px; - background-color: #EEEEEE; - border: 1px solid #C9C9C9; - border-radius: 6px 6px 6px 6px; - box-shadow: 1px 1px 7px #CACACA; -} - -div.metadata p { - margin: 7px 0px 7px 10px; -} - -span.def { - font-family: monospace; - font-size: 90%; -} - -span.argspec-highlight { - color: red; - font-size: 110%; - font-weight: 900; -} - - -/* --- Docstring div style --- */ -div.docstring { - margin-top: -1px; -} - -div.docstring p { - padding: 0px 2px 0px; -} - - -/* --- Headers style --- */ -h2, h3, h4 { - font-family: 'Helvetica', sans-serif; - color: rgb(49, 126, 172); - margin-top: 20px; - margin-bottom: 10px; -} - -h2 { - font-size: 140%; - font-weight: normal; - border-bottom: 1px solid rgb(220, 220, 220); - padding: 4px 0px 4px 0px; -} - -h3 { - font-size: 115%; -} - -h4 { - font-size: 100%; - margin-top: 14px; - font-weight: normal; -} - -h2.html-toggle-button, h3.html-toggle-button, h4.html-toggle-button { - padding-left: 20px; -} - -.collapsed > h2, .collapsed > h3, .collapsed > h4, .expanded > h2, .expanded > h3, .expanded > h4 { - background-color: transparent; - background-image: url(../images/collapse_expand.png); - background-repeat: no-repeat; - background-attachment: scroll; - cursor: pointer; -} - -.collapsed > h2 { - background-position: 2px 7px; -} - -.collapsed > h3 { - background-position: 2px 2px; -} - -.collapsed > h4 { - background-position: 2px 0px; -} - -.expanded > h2 { - background-position: 0px -31px; -} - -.expanded > h3 { - background-position: 0px -38px; -} - -.expanded > h4 { - background-position: 0px -39px; -} - -dl.docutils { - padding: 0px 10px 0px; -} - -div.section p { - padding: 0px 2px 0px; -} - -#warning { - margin-top: 5px; - background-color: #FFE4E4; - border: 1px solid #F66; - padding: 4px 8px 4px 8px; - text-align: center; -} - -#doc-warning { - margin-top: 16px; - width: 45%; - margin-left: auto; - margin-right: auto; - color: rgb(185, 74, 72); - background-color: rgb(242, 222, 222); - border: 1px solid rgb(238, 211, 215); - border-radius: 4px 4px 4px 4px; - padding: 15px; - text-align: center; - font-weight: bold; - font-size: 105%; -} - - -/* --- Links --- */ -a { - text-decoration: none; - color: rgba(40, 130, 180, 1); -} - -a:hover { - text-decoration: underline; -} - - -/* --- Images --- */ -img { - box-shadow: 0px 2px 6px #cacaca; - border: 1px solid #c9c9c9; -} - -img.align-center { - display: block; - margin-left: auto; - margin-right: auto; -} - - -/* --- Lists style --- */ -ol.arabic { - margin-left: -10px; -} - -ul { - margin-left: -5px; -} - -/* --- Literal blocks style --- */ -pre.literal-block { - padding-left: 35px; - font-size: 95%; -} - - -/* --- Docutils table style --- */ -table.docutils { - border-collapse: collapse; - border-spacing: 0; - border: #DDDDDD; - margin-left: auto; - margin-right: auto; - margin-top: 17px; - margin-bottom: 17px; - width: 90%; -} - -table.docutils td { - padding: 5px; -} - -table.docutils tr.row-odd { - background-color: rgb(249, 249, 249); -} - - -/* --- Docutils table headers --- */ -table.docutils th { - background-color: #EEEEEE; - border-bottom-color: #DDDDDD; - border-bottom-style: solid; - border-bottom-width: 1px; - border-top-color: #DDDDDD; - border-top-style: solid; - border-top-width: 1px; - font-weight: bold; - text-align: center; - padding: 6px 0px 6px 8px; - color: rgb(65, 65, 65); -} - - -/* --- Field-list table style --- */ -table.docutils.field-list { - font-size: 80%; - border-collapse: collapse; - border-left: transparent; - border-right: transparent; - margin-top: 15px; - margin-left: 40px; - width: 83%; -} - - -/* --- Field-list table headers --- */ -table.docutils.field-list th { - background-color: transparent; - border-top: transparent; - border-bottom: transparent; - color: black; - font-weight: bold; - text-align: left; - padding: 4px 0px 4px 8px; -} - - -/* --- Spacing around example code --- */ -div.highlight pre { - padding: 9px 14px; - background-color: rgb(247, 247, 249); - border-radius: 4px 4px 4px 4px; - border: 1px solid rgb(225, 225, 232); -} - -div.highlight { - padding: 0px 10px 0px; -} - -dt { - font-weight: bold; - /*font-size: 16px;*/ -} - -.classifier { - /*font-size: 10pt;*/ - font-weight: normal; -} - -tt { - background-color: #ECF0F3; - /*font-size: 95%;*/ - padding: 0px 1px; -} - - - -div.admonition.note { - font-size: 0.95em; - margin: 1.3em; - border: 1px solid #BCE8F1; - background-color: #D9EDF7; - padding: 0px 5px 0 5px; - color: #3A87AD; -} - -div.admonition p.admonition-title { - font-size: 1em; - margin-top: 7px; - font-weight: bold; -} - - -/* ----------- Panels ----------- */ - -.panel { - margin-top: 15px; - background-color: #ffffff; - border: 1px solid transparent; - border-radius: 4px; - box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05); -} - -.panel-heading { - padding: 10px 15px; - border-bottom: 1px solid transparent; - border-top-right-radius: 3px; - border-top-left-radius: 3px; -} - -.panel-title { - margin-top: 0; - margin-bottom: 0; - font-size: 110%; - color: rgb(255,255,255); -} - -.panel-body { - padding: 15px; -} - -.panel-usage { - border-color: #2fa4e7; - margin-top: 15px; - width: 60%; - margin-left: auto; - margin-right: auto; -} - -.panel-usage > .panel-heading { - color: #ffffff; - background-color: #2fa4e7; - border-color: #2fa4e7; -} - -.panel-usage > .panel-body > br { - display: block; - margin: 12px 0; - content: ""; -} - -.hr { - background-color: rgb(200, 200, 200); - height: 1px; -} - - -/* ----------- IPython console styles ----------- */ - -/* --- Loading --- */ -.loading { - position: absolute; - margin: -20px 0 0 -95px; - width: 180px; - height: auto; - left: 50%; - top: 50%; - background-color: #EEEEEE; - border: 1px solid #C9C9C9; - border-radius: 6px; - box-shadow: 0px 0px 7px #CACACA; - color: #333333; - padding: 12px; - text-align: center; -} - -#loading-image { - float: left; -} - -#loading-message { - margin-left: 23px; -} - -/* --- Kernel error messages --- */ -.panel-danger { - border-color: #eed3d7; -} - -.panel-danger > .panel-heading { - color: #b94a48; - background-color: #f2dede; - border-color: #eed3d7; - background-color: rgb(199, 28, 34); -} diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/utils/inspector/static/css/pygments.css spyder-3.0.2+dfsg1/spyder/utils/inspector/static/css/pygments.css --- spyder-2.3.8+dfsg1/spyder/utils/inspector/static/css/pygments.css 2015-08-24 21:09:46.000000000 +0200 +++ spyder-3.0.2+dfsg1/spyder/utils/inspector/static/css/pygments.css 1970-01-01 01:00:00.000000000 +0100 @@ -1,61 +0,0 @@ -.hll { background-color: #ffffcc } -.c { color: #408090; font-style: italic } /* Comment */ -.err { border: 1px solid #FF0000 } /* Error */ -.k { color: #007020; font-weight: bold } /* Keyword */ -.o { color: #666666 } /* Operator */ -.cm { color: #408090; font-style: italic } /* Comment.Multiline */ -.cp { color: #007020 } /* Comment.Preproc */ -.c1 { color: #408090; font-style: italic } /* Comment.Single */ -.cs { color: #408090; background-color: #fff0f0 } /* Comment.Special */ -.gd { color: #A00000 } /* Generic.Deleted */ -.ge { font-style: italic } /* Generic.Emph */ -.gr { color: #FF0000 } /* Generic.Error */ -.gh { color: #000080; font-weight: bold } /* Generic.Heading */ -.gi { color: #00A000 } /* Generic.Inserted */ -.go { color: #303030 } /* Generic.Output */ -.gp { color: #c65d09; font-weight: bold } /* Generic.Prompt */ -.gs { font-weight: bold } /* Generic.Strong */ -.gu { color: #800080; font-weight: bold } /* Generic.Subheading */ -.gt { color: #0040D0 } /* Generic.Traceback */ -.kc { color: #007020; font-weight: bold } /* Keyword.Constant */ -.kd { color: #007020; font-weight: bold } /* Keyword.Declaration */ -.kn { color: #007020; font-weight: bold } /* Keyword.Namespace */ -.kp { color: #007020 } /* Keyword.Pseudo */ -.kr { color: #007020; font-weight: bold } /* Keyword.Reserved */ -.kt { color: #902000 } /* Keyword.Type */ -.m { color: #208050 } /* Literal.Number */ -.s { color: #4070a0 } /* Literal.String */ -.na { color: #4070a0 } /* Name.Attribute */ -.nb { color: #007020 } /* Name.Builtin */ -.nc { color: #0e84b5; font-weight: bold } /* Name.Class */ -.no { color: #60add5 } /* Name.Constant */ -.nd { color: #555555; font-weight: bold } /* Name.Decorator */ -.ni { color: #d55537; font-weight: bold } /* Name.Entity */ -.ne { color: #007020 } /* Name.Exception */ -.nf { color: #06287e } /* Name.Function */ -.nl { color: #002070; font-weight: bold } /* Name.Label */ -.nn { color: #0e84b5; font-weight: bold } /* Name.Namespace */ -.nt { color: #062873; font-weight: bold } /* Name.Tag */ -.nv { color: #bb60d5 } /* Name.Variable */ -.ow { color: #007020; font-weight: bold } /* Operator.Word */ -.w { color: #bbbbbb } /* Text.Whitespace */ -.mf { color: #208050 } /* Literal.Number.Float */ -.mh { color: #208050 } /* Literal.Number.Hex */ -.mi { color: #208050 } /* Literal.Number.Integer */ -.mo { color: #208050 } /* Literal.Number.Oct */ -.sb { color: #4070a0 } /* Literal.String.Backtick */ -.sc { color: #4070a0 } /* Literal.String.Char */ -.sd { color: #4070a0; font-style: italic } /* Literal.String.Doc */ -.s2 { color: #4070a0 } /* Literal.String.Double */ -.se { color: #4070a0; font-weight: bold } /* Literal.String.Escape */ -.sh { color: #4070a0 } /* Literal.String.Heredoc */ -.si { color: #70a0d0; font-style: italic } /* Literal.String.Interpol */ -.sx { color: #c65d09 } /* Literal.String.Other */ -.sr { color: #235388 } /* Literal.String.Regex */ -.s1 { color: #4070a0 } /* Literal.String.Single */ -.ss { color: #517918 } /* Literal.String.Symbol */ -.bp { color: #007020 } /* Name.Builtin.Pseudo */ -.vc { color: #bb60d5 } /* Name.Variable.Class */ -.vg { color: #bb60d5 } /* Name.Variable.Global */ -.vi { color: #bb60d5 } /* Name.Variable.Instance */ -.il { color: #208050 } /* Literal.Number.Integer.Long */ \ Kein Zeilenumbruch am Dateiende. Binärdateien spyder-2.3.8+dfsg1/spyder/utils/inspector/static/images/collapse_expand.png und spyder-3.0.2+dfsg1/spyder/utils/inspector/static/images/collapse_expand.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/utils/inspector/static/images/debug-continue.png und spyder-3.0.2+dfsg1/spyder/utils/inspector/static/images/debug-continue.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/utils/inspector/static/images/debug-step-in.png und spyder-3.0.2+dfsg1/spyder/utils/inspector/static/images/debug-step-in.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/utils/inspector/static/images/debug-step-out.png und spyder-3.0.2+dfsg1/spyder/utils/inspector/static/images/debug-step-out.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/utils/inspector/static/images/debug-step-over.png und spyder-3.0.2+dfsg1/spyder/utils/inspector/static/images/debug-step-over.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/utils/inspector/static/images/spyder-hello-docstring.png und spyder-3.0.2+dfsg1/spyder/utils/inspector/static/images/spyder-hello-docstring.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/utils/inspector/static/images/spyder-nice-docstring-rendering.png und spyder-3.0.2+dfsg1/spyder/utils/inspector/static/images/spyder-nice-docstring-rendering.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder/utils/inspector/static/images/spyder-sympy-example.png und spyder-3.0.2+dfsg1/spyder/utils/inspector/static/images/spyder-sympy-example.png sind verschieden. diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/utils/inspector/templates/layout.html spyder-3.0.2+dfsg1/spyder/utils/inspector/templates/layout.html --- spyder-2.3.8+dfsg1/spyder/utils/inspector/templates/layout.html 2015-08-24 21:09:46.000000000 +0200 +++ spyder-3.0.2+dfsg1/spyder/utils/inspector/templates/layout.html 1970-01-01 01:00:00.000000000 +0100 @@ -1,85 +0,0 @@ -{# - layout.html - ~~~~~~~~~~~ - - Layout template for the object inspector - - :copyright: Copyright 2013 by the Spyder Development Team. - :copyright: Copyright 2009 by Tim Dumol - :license: BSD license -#} - - - - - - - - - - - - {% if right_sphinx_version and math_on %} - {# DON'T try to load MathJax from the net. It's slow and sometimes gives - errors. See this thread for more info: - http://tex.stackexchange.com/questions/2692/comparing-mathjax-and-mathml - #} - - {% endif %} - - - - - - - - -{% if collapse %} - - -{% endif %} - -{% if img_path %} - -{% endif %} - - - - {# Docstring header #} - {% if name %} -

    {{name}}

    - - {% if argspec or note %} - - {% endif %} - - {% endif %} - - {# Docstring text #} -
    - {% block body %}{% endblock %} - {% if collapse %} -
    -

    Outline

    - {{ toc }} -
    - {% endif %} -
    - - - diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/utils/inspector/templates/usage.html spyder-3.0.2+dfsg1/spyder/utils/inspector/templates/usage.html --- spyder-2.3.8+dfsg1/spyder/utils/inspector/templates/usage.html 2015-08-24 21:09:46.000000000 +0200 +++ spyder-3.0.2+dfsg1/spyder/utils/inspector/templates/usage.html 1970-01-01 01:00:00.000000000 +0100 @@ -1,39 +0,0 @@ -{# - usage.html - ~~~~~~~~~~ - - A simple page to inform users how to get help on the Object Inspector - - :copyright: Copyright 2013 by the Spyder Development Team. - :license: MIT license -#} - - - - - - - - - - - -
    -
    -
    {{title}}
    -
    -
    - {{intro_message}} -

    -
    -
    -
    - {{tutorial_message}} - {{tutorial}} -
    -
    -
    - - - diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/utils/inspector/templates/warning.html spyder-3.0.2+dfsg1/spyder/utils/inspector/templates/warning.html --- spyder-2.3.8+dfsg1/spyder/utils/inspector/templates/warning.html 2015-08-24 21:09:46.000000000 +0200 +++ spyder-3.0.2+dfsg1/spyder/utils/inspector/templates/warning.html 1970-01-01 01:00:00.000000000 +0100 @@ -1,12 +0,0 @@ -{# - warning.html - ~~~~~~~~~~~ - - A simple page to emit a warning when no docstring text was found for the - one a user was looking for - - :copyright: Copyright 2012 by the Spyder team. - :license: MIT license -#} - -
    {{text}}
    diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/utils/inspector/tutorial.rst spyder-3.0.2+dfsg1/spyder/utils/inspector/tutorial.rst --- spyder-2.3.8+dfsg1/spyder/utils/inspector/tutorial.rst 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/utils/inspector/tutorial.rst 1970-01-01 01:00:00.000000000 +0100 @@ -1,950 +0,0 @@ -====================================================== -Spyder - the Scientific PYthon Development EnviRonment -====================================================== - -*Spyder* is an Integrated Development Environment (IDE) for scientific -computing using the Python programming language. It comes with an -Editor to write code, a Console to evaluate it and see its results at -any time, a Variable Explorer to see what variables have been defined -during evaluation, and several other facilities to help you to -effectively develop the programs you need as a scientist. - - -This tutorial is authored by -`Hans Fangohr `__ from the -University of Southampton (UK) (see `historical note`_ for more -detail). - - -First steps with Spyder -####################### - -This section is aimed at Python and Spyder beginners. If you find it too -simple, please continue to the next section. - -Execute a given program ------------------------ - -* We are going to use this program as a first example: - - .. code-block:: python - - # Demo file for Spyder Tutorial - # Hans Fangohr, University of Southampton, UK - - def hello(): - """Print "Hello World" and return None""" - print("Hello World") - - # main program starts here - hello() - -* To use this program, please create a new file in the Spyder editor pane. Then copy - and paste the code inside the box above on the file, and the save it with the name - ``hello.py``. - -* To execute the program, select ``Run > Run`` from the menu (or press F5), and - confirm the ``Run settings`` if required. - - If this is your first time, you should see an output like this:: - - In [1]: runfile('/Users/fangohr/Desktop/hello.py', wdir=r'/Users/fangohr/Desktop') - Hello World - - In [2]: - - If so, then you have just run your first Python program - well done. - - .. note:: - - The particular path shown next to ``runfile`` will depend on where you have saved - the file, but this is inserted by Spyder automatically. - - -Use the IPython Console -~~~~~~~~~~~~~~~~~~~~~~~ - -Before we proceed, we recommend you to use the IPython console. This console can do a -little more than the standard Python console, and we suggest to use it as the default -console here. - - -What happens when you execute the program? -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -* Python reads the file line by line, ignoring comments (i.e. lines starting - with the ``#`` symbol). - -* When it comes across the ``def`` keyword, it knows that a function - is DEFined in this and the next (one or more) lines. All *indented* lines - following ``def hello():`` belong to the function body. - - Note that the function object is just created at this point in the - file, but the function is not yet called (i.e. not executed). - -* When Python comes across commands (other than ``def ...`` and a few - other keywords) that are written in the left-most column, it will - execute these immediately. In the ``hello.py`` file this is only the - line reading ``hello()`` which will actually call (i.e. *execute*) - the function with name ``hello``. - - If you comment or remove the line ``hello()`` from the program and run - the whole file again (by pressing F5, or selecting ``Run > Run``), nothing - will be printed (because the function ``hello`` is defined, but not called, - i.e. not executed). - - -Now you should know how to execute a Python program that you have in -the editor pane in Spyder using the IPython console. - -If you are just starting to learn Python, this is probably a good -point to return to your text book / course and look at more basic -examples. - - -The next section gives more detailed information how you can execute -*parts* of the code in the editor in the IPython console, and thus -update parts of your definitions in the editor. This is a more -advanced technique but can be very useful. (You may also be interested -in the option to execute chunks (so-called "cells") of code that are -separated by delimiters -- see `Shortcuts for useful functions`_.) - - - -Call existing functions in the console --------------------------------------- - -Once you have executed the ``hello.py`` program, the function object ``hello`` -is defined and known to the IPython console. We can thus call the function from -the console like this: - -* Type ``hello()`` in the console (next to ``In [?]`` prompt, where - the question mark can be any positive integer number), and press the - ``Enter`` key. - - You should find that the ``hello()`` function is executed again, - i.e. ``Hello World`` is printed again. Your function call at the - console together with the output should look like this:: - - In [ ]: hello() - Hello World - -* Can you see how this differs from executing the whole program again? - - When we execute the whole program (by pressing F5), Python goes - through the file, creates the ``hello`` function object (overriding - the previous object), reaches the ``hello()`` line and calls the - function. - - When we call ``hello()`` in the console, we only call the - function object ``hello`` that has been defined in the IPython - console when we executed the whole ``hello.py`` file earlier (by - pressing F5). - - This will become clearer over time and also when we work with - slightly larger examples. You may want to return to this tutorial at - a slightly later stage. - - -Inspecting objects defined in the console ------------------------------------------ - -* Python provides a function that displays all known objects in the - current name space of the console. It is called ``dir()``: when you - type ``dir()`` at the console, you get a list of known objects. Ignore - everything starting with an underscore for now. Can you see ``hello`` - in the list? - - .. note:: - - If you get a long list of defined objects, then Spyder may have - done some convenience imports for you already. To address this you - may want to: - - - `Reset the name space`_ - - - Execute ``hello.py`` again by pressing F5 - - Then run ``dir()`` as suggested above. - -* Once an object is visible in the current name space (as is ``hello`` - in this example), we can use the ``help`` function as follows to - learn about it: Typing ``help(hello)`` at the console prompt, you - should see an output like this:: - - In [ ]: help(hello) - Help on function hello in module __main__: - - hello() - Print "Hello World" and return None - - - Where does Python take the information from? Some of it (like the - number of input arguments and names of those variables; here we have - no input arguments) Python can find through inspecting its objects, - additional information comes from the documentation string provided - for the function object ``hello``. The documentation string is the - first string immediately below the line ``def hello():``. - - This strings are special, and they are called *docstrings* which is short for - *documentation strings*. As they usually extend over multiple lines, there - are enclosed by triple single quotes (``'''``) or triple double quotes - (``"""``). - -* The Spyder environment also provides the ``Object inspector`` which - by default is located in the top right corner. - - While the cursor is on the name of an object, - press ``CTRL+i`` (or ``CMD+i`` on Mac), and you should find that - the same information as we obtained from ``help(hello)`` is provided - automatically in the object inspector: - - .. image:: static/images/spyder-hello-docstring.png - :align: center - - This works in the console and in the editor. - -Updating objects ----------------- - -Simple strategy: re-execute whole program -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -* In the Editor window, change the function ``hello`` so - that it prints ``Good Bye World`` rather than ``Hello World``. - -* Press F5 (to execute the whole program) and check that the output of the - program is now:: - - Good Bye World - -What has happened when you pressed F5 is this: Python has gone through -the ``hello.py`` file and created a new function object ``hello`` -(overriding the function object ``hello`` we had defined before) and -then executed the function. - - -Looking at the details -~~~~~~~~~~~~~~~~~~~~~~ - -We need to start with a clearly defined state. To do this, please change the -function ``hello()`` back so that it prints ``Hello World``, then press -F5 to run the whole program and check that it prints ``Hello World``. - -* Call the function ``hello()`` from the command prompt (as described - in `Call existing functions in the console`_). You - should see ``Hello World`` printed. - -* Now change the function definition so that it would print ``Laters - World``, and save the file (but do NOT execute the program, i.e. do - NOT press F5 yet). - -* Call the function ``hello()`` in the console again. You - should find that the text printed reads ``Hello World``, like here - :: - - In [ ]: hello() - Hello World - - Why is this so? Because the ``hello`` function object in the console - is the old one which prints ``Hello World``. So far, we have - changed the file ``hello.py`` (and replaced ``Hello World`` in there with - ``Laters World``) in the editor but this has not affected the objects that - have previously been created in the console. - -Here are two possibilities to use our modified version of the ``hello`` -function: - -* Option 1: execute the whole file ``hello.py`` again by pressing F5: - this creates a new function object ``hello`` (and overrides the old - one). You should find that if you press F5, and then call - ``hello()`` at the prompt, the new text ``Laters World`` is printed. - -* Option 2: select the region you have changed (in this case the whole - function ``hello``, starting from the line ``def hello():`` down to - ``print("Laters Wold")``, and then select ``Run > Run selection``. - - This will update the ``hello`` object in the console without - having to execute the whole ``hello.py`` file:: - - In [ ]: def hello(): - ...: """Print "Hello World" and return None""" - ...: print("Laters world") - ...: - - If we now type ``hello()``, we see the update response:: - - In [ ]: hello() - Laters world - -The ability to execute *parts of the code* to update some objects in -the console (in the example above, we updated the function object -``hello``), is of great use when developing and debugging more complex -codes, and when creating objects/data in the console session take -time. For example, by modifying only the functions (or -classes/objects, etc) that we are actually developing or debugging, we -can keep re-using the data and other objects that are defined in the -console session. - - - -Recommended first steps for Python beginners -############################################ - -To teach and learn Python programming, we recommend here to use IPython -instead of the normal Python console. This accepts IPython as the -de-facto standard in the scientific Python community. - -Switch to an IPython console ----------------------------- - -If you already have an IPython console active, you can ignore this section, -and make it visible by clicking on the "IPython console" rider. - -In the console window (lower right corner by default), you see by -default a prompt with three greater than signs, i.e. ``>>>``. This -shows that we are using the ``console`` -- basically a normal Python -console session (with some added functionality from Spyder). - -Instead, we would like to use an *Interactive Python* console, short *IPython* -from the `IPython project `__. To do this, select -``Consoles > Open an IPython Console``. - -You should see in the consolse window a new shell appearing, and the -IPython prompt ``In [1]:`` should be displayed. - -Reset the name space --------------------- - -The `name space `__ -(i.e. the collection of objects defined in the console at any given time) -can be cleared in IPython using the ``%reset`` command. Type ``%reset`` -and press return, then confirm with ``y``:: - - In [1]: %reset - - Once deleted, variables cannot be recovered. Proceed (y/[n])? y - - In [2]: - -That's all. - -We discuss this a little further, but you can skip the following if -you are not interested: After issuing the ``%reset`` command, we -should have only a few objects defined in the name space of that -session. We can list all of them using the ``dir()`` command:: - - In [2]: dir() - Out[2]: - ['In', - 'Out', - '__builtin__', - '__builtins__', - '__name__', - '_dh', - '_i', - '_i2', - '_ih', - '_ii', - '_iii', - '_oh', - '_sh', - 'exit', - 'get_ipython', - 'help', - 'quit'] - -Finally, if you like to skip the confirmation step of the ``reset`` -command, use can use ``%reset -f`` instead of ``%reset``. - -Strive for PEP8 Compliance --------------------------- - -In addition to the syntax that is enforced by the Python programming -language, there are additional conventions regarding the layout of -the source code, in particular the `Style Guide for Python source code -`__ known as "PEP8". By -following this guide and writing code in the same style as almost all -Python programmers do, it becomes easier to read, and thus easier to -debug and re-use -- both for the original author and others. - -You should change Spyders settings to -`Warn if PEP8 coding guidelines are violated`_. - - - -Selected Preferences -#################### - -Where are the preferences? --------------------------- - -A lot of Spyder's behaviour can be configured through it's -Preferences. Where this is located in the menu depends on your -operating system: - -* On Windows and Linux, go to ``Tools > Preferences`` - -* On Mac OS, go to ``Python/Spyder > Preferences`` - -Warn if PEP8 coding guidelines are violated -------------------------------------------- - -Go to ``Preferences > Editor > Code -Introspection/Analysis`` and -select the tickbox next to ``Style analysis (PEP8)`` - -Automatic Symbolic Python -------------------------- - -Through ``Preferences > IPython console > Advanced Settings > Use -symbolic math`` we can activate IPython's SYMbolic PYthon (sympy) mode that is -provided by the `sympy `__ module. This mode -in Spyder allows nicely rendered mathematical output (latex style) and also -imports some sympy objects automatically when the IPython console starts, and -reports what it has done. - -.. code-block:: python - - These commands were executed: - >>> from __future__ import division - >>> from sympy import * - >>> x, y, z, t = symbols('x y z t') - >>> k, m, n = symbols('k m n', integer=True) - >>> f, g, h = symbols('f g h', cls=Function) - -We can then use the variables ``x``, ``y``, for example like this: - -.. image:: static/images/spyder-sympy-example.png - :align: center - - - -Shortcuts for useful functions -############################## - -- ``F5`` executes the current file - -- ``F9`` executes the currently highlighted chunk of code: this is very useful - to update definitions of functions (say) in the console session without - having to run the whole file again. If nothing is selected ``F9`` executes - the current line. - -- ``Tab`` auto-completes commands, function names, variable - names, methods in the Console (both Python and IPython) and in the - Editor. This feature is very useful, and should be used - routinely. Do try it now if auto-completion is new to you. - Assume you have defined a variable:: - - mylongvariablename = 42 - - Suppose we need to write code that computes ``mylongvariablename + - 100``, we can simply type ``my`` and then press the ``Tab`` key. The - full variable name will be completed and inserted at the cursor - position if the name is unique, and then we can carry on and type - ``+ 100``. If the name is not uniquely identifiable given the - letters ``my``, a list field will be displayed from which the right - variable can be chosen. Choosing from the list can be done with the - ```` key and ```` key and the ``Enter`` - key to select, or by typing more letters of the name in question - (the selection will update automatically) and confirming by pressing - ``Enter`` when the right name is identified. - -- ``Ctrl+Enter`` executes the current cell (menu enty ``Run > Run - cell``). A cell is defined as the code between two lines which start with - the agreed tag ``#%%``. - -- ``Shift+Enter`` executes the current cell and advances the - cursor to the next cell (menu entry ``Run > Run cell and - advance``). - - Cells are useful to execute a large file/code segment in smaller - units. (It is a little bit like a cell in an IPython notebook, in - that chunks of code can be run independently.) - -- ``Alt+`` moves the current line up. If multiple lines are - highlighted, they are moved up together. ``Alt+`` - works correspondingly moving line(s) down. - -- ``Ctrl+Left Mouse Click`` on a function/method in the source, opens a new - editor windows showing the definition of that function. - -- ``Shift+Ctrl+Alt+M`` maximizes the current window (or changes the - size back to normal if pressed in a maximized window) - -- ``Ctrl+Shift+F`` activates the search pane across all files. - -- ``Cmd + +`` (On MacOS X) or ``Ctrl + +`` (otherwise) will increase the font - size in the Editor, whereas ``Cmd + -`` (``Ctrl + -``) will decrease it. - Also works in the IPython Console. - - The font size for the Object Inspector, the Python console etc can be set - individually via ``Preferences > Object inspector`` etc. - - I couldn't find a way of changing the font size in the variable explorer. - -- ``Cmd+S`` (on MacOS X) and ``Ctrl+S`` (otherwise) *in the Editor* - pane saves the file - currently being edited. This also forces various warning triangles - in the left column of the Editor to be updated (otherwise they - update every 2 to 3 seconds by default). - -- ``Cmd+S`` (on MacOS X) and ``Ctrl+S`` (otherwise) *in the IPython console* - pane saves the current IPython session as an HTML file, including - any figures that may be displayed inline. This is useful as a quick - way of recording what has been done in a session. - - (It is not - possible to load this saved record back into the session - if you - need functionality like this, look for the IPython Notebook.) - -- ``Cmd+I`` (on Mac OS X) and ``Ctrl+I`` (otherwise) when pressed - while the cursor is on an object, opens documentation for that - object in the object inspector. - - - -Run Settings -############ - -These are the settings that define how the code in the editor is -executed if we select ``Run > Run`` or press F5. - -By default, the settings box will appear the first time we try to execute a -file. If we want to change the settings at any other time, they can be -found under ``Run > Configure`` or by pressing F6. - -There are three choices for the console to use, of which I'll discuss the -first two. Let's assume we have a program ``hello.py`` in the editor which -reads - -.. code-block:: python - - def hello(name): - """Given an object 'name', print 'Hello ' and the object.""" - print("Hello {}".format(name)) - - - i = 42 - if __name__ == "__main__": - hello(i) - -Execute in current Python or IPython console --------------------------------------------- - -This is the default suggestion, and also generally a good choice. - -Persistence of objects I (after code execution) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Choosing ``Execute in current Python or IPython console`` -setting under ``Run > Configure`` means that - -1. When the execution of ``hello.py`` is completed, we can interact - with the console in which the program ran, and we can use the - convenient IPython console for this (rather than the default - Python console). - - In particular, - -2. we can inspect and interact with objects that the execution of - our program created, such as ``i`` and ``hello()``. - -This is generally very useful for incremental coding, testing and -debugging: we can call ``hello()`` directly from the console -prompt, and don't need to execute the whole ``hello.py`` for this -(although if we change the function ``hello()``, we need to execute -the file, or at least the function definition, to make the new -version of ``hello()`` visible at the console; either by -executing the whole buffer or via ``Run > Run Selection``.) - -Persistence of objects II (from before code execution) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -However, executing the code in the editor in the current console -also means that - -3. the code that executes can see other (global) objects that were - defined in the console session. - -*This* persistence of objects is easily forgotten and usually not -required when working on small programs (although it can be of great -value occasionally). These objects could come from previous execution -of code, from interactive work in the console, or from convenience -imports such as ``from pylab import *`` (Spyder may do some of those -convenience imports automatically). - -This visibility of objects in the console name space to the -code we execute may also result in coding mistakes if the code -inadvertently relies on these objects. - -Here is an example: imagine that - -* we run the code ``hello.py``. Subsequently, the variable ``i`` - is known in the console as a global variable. - -* we edit the ``hello.py`` source and accidentally delete the line - ``i = 42`` - -* we execute the buffer containing ``hello.py`` again. At this - point, the call of ``hello(i)`` will *not* fail because the - console has an object of name ``i`` defined, although this is - not defined in the source of ``hello.py``. - -At this point, we could save ``hello.py`` and (falsely) think it -would execute correctly. However, running it in a new (I)Python -console session (or via ``python hello.py`` in a terminal, say) -would result in an error, because ``i`` is not defined. - -The problem arises because the code makes use of an object (here -``i``) without creating it. This also affects importing of modules: if -we had imported ``pylab`` at the IPython prompt, then our program will -see that when executed in this IPython console session. - -To learn how we can double check that our code does not depend on such -existing objects, see `How to double check your code executes correctly "on -its own"`_ . - -Execute in new dedicated Python console ---------------------------------------- - -Choosing ``Execute in new dedicated Python console`` under ``Run -> Configure`` will start *a new Python console everytime* the -``hello.py`` program is executed. The major advantage of this mode -over `Execute in current Python or IPython console`_ is that we -can be certain that there are no global objects defined in this -console which originate from debugging and repeated execution of -our code: every time we run the code in the editor, the python -console in which the code runs is restarted. - -This is a safe option, but provides less flexibility and cannot use -the IPython console. - -How to double check your code executes correctly "on its own" -------------------------------------------------------------- - -Assuming you have chosen for your code to -`Execute in current Python or IPython console`_, -then you have two options to check that your code does work on its own -(i.e. it does not depend on undefined variables, unimported modules -and commands etc.) - -(i) Switch from `Execute in current Python or IPython console`_ - to `Execute in new dedicated Python console`_, - and execute the code in the editor in this dedicated Python console. - - Alternatively, if you want to stay with the current IPython - console, you can - -(ii) Use IPython's magic ``%reset`` command which will remove all - objects (such as ``i`` in the example above) from the current name - space, and then execute the code in the editor. - -Recommendation --------------- - -My recommendation for beginners would be to -`Execute in current Python or IPython console`_, *and* -to choose the IPython console for this. - -Once you have completed a piece of code, double check that it executes -independently using one of the options explained in -`How to double check your code executes correctly "on its own"`_\ . - - - -Other observations -################## - -Multiple files --------------- - -When multiple files are opened in the Editor, the -corresponding tabs at the top of the window area are arranged in -alphabetical order of the filename from left to right. - -On the left of the tabs, there is as icon that shows ``Browse tabs`` -if the mouse hovers over it. It is useful to jump to a particular -file directly, if many files are open. - -Environment variables ---------------------- - -Environment variables can be displayed from the Python Console window (bottom -right window in default layout). Click on the ``Options`` icon (the tooltip is -``Options``), then select ``Environment variables``. - -Reset all customization ------------------------ - -All customization saved on disk can be reset by calling spyder from -the command line with the switch ``--reset``, i.e. a command like -``spyder --reset``. - -Objects in the variable explorer --------------------------------- - -Right-clicking on arrays in the variable explorer gives options to -plot and analyze these further. - -Double clicking on a dictionary object opens a new window that -displays the dictionary nicely. - -You can also show and edit the contents of numpy arrays, lists, numbers -and strings. - - - -Documentation string formatting -############################### - -If you want to add documentation for the code you are developing, we recommend -you to write documentation strings (or *docstrings*) for it, using a special -format called restructured text (`quick reference -`__). This format -also needs to follow a set of conventions called the `Numpydoc standard -`__ - -If you follow those guidelines, you can obtain beautifully formated docstrings -in Spyder. - -For example, to get an ``average()`` function look like this in the -Spyder Object inspector: - -.. image:: static/images/spyder-nice-docstring-rendering.png - :align: center - -you need to format the documentation string as follows - -.. code-block:: python - - def average(a, b): - """ - Given two numbers a and b, return their average value. - - Parameters - ---------- - a : number - A number - b : number - Another number - - Returns - ------- - res : number - The average of a and b, computed using 0.5*(a + b) - - Example - ------- - >>> average(5, 10) - 7.5 - - """ - - return (a + b) * 0.5 - -What matters here, is that the word ``Parameters`` is used, and -underlined. The line ``a : number`` shows us that the type of the -parameter ``a`` is ``number``. In the next line, which is indented, we -can write a more extended explanation of what this variable represents, -what conditions the allowed types have to fulfill, etc. - -The same for all Parameters, and also for the returned value. - -Often it is a good idea to include an example too, as shown. - - - -Debugging -######### - -Line by line step execution of code ------------------------------------ - -Activating the debug mode (with the ``Debug > Debug`` menu option or Ctrl+F5) -starts the Python debugger (Pdb) if the Python console is active, or the IPython -debugger (ipdb) if the IPython console is active. After doing that, the -Editor pane will highlight the line that is about to be executed, and the -Variable Explorer will display variables in the current context of the point -of program execution. (It only displays 'numerical' and array type of variables, -i.e. not function or class objects) - -After entering debug mode, you can execute the code line by line using the -``Step`` button of the Debug toolbar: - -.. image:: static/images/debug-step-over.png - :align: center - -or the shortcut Ctrl+F10. You can also inspect how a particular function is -working by stepping into it with the ``Step into`` button - -.. image:: static/images/debug-step-in.png - :align: center - -or the shortcut Ctrl+F11. Finally, to get out of a function and continue with -the next line you need to use the ``Step return`` button - -.. image:: static/images/debug-step-out.png - :align: center - -or the shortcut Ctrl+F12. - -If you prefer to inspect your program at a specific point, you need to insert a -*breakpoint* by pressing F12 on the line on which you want to stop. After -that a red dot will be placed next to the line and you can press the ``Continue`` -button - -.. image:: static/images/debug-continue.png - :align: center - -(after entering debug mode) to stop the execution at that line. - -.. note:: - - You can also control the debugging process by issuing these commands in the - console prompt: - - * ``n`` to move to the Next statement. - - * ``s`` to Step into the current statement. If this is a function - call, step into that function. - - * ``r`` to complete all statements in the current function and Return - from that function before returning control. - - * ``p`` to print values of variables, for example ``p x`` will print the - value of the variable ``x``. - -At the debugger prompt, you can also *change* values of variables. For -example, to modify a variable ``x`` at the IPython debugger prompt, you can say -``ipdb > x = 42`` and the debugger will carry on with ``x`` being bound to ``42``. -You can also call functions, and do many others things. Try this example:: - - def demo(x): - for i in range(5): - print("i={}, x={}".format(i, x)) - x = x + 1 - - demo(0) - -If we execute this (``Run > Run``), we should see the output:: - - i=0, x=0 - i=1, x=1 - i=2, x=2 - i=3, x=3 - i=4, x=4 - -Now execute this using the debugger (``Debug > Debug``), press the -``Step button`` until the highlighted line reaches the ``demo(0)`` -function call, then press the ``Step into`` to inspect this function. -Keep pressing the ``Step button`` to execute the next lines. Then, -modify ``x`` by typing ``x=10`` in the debugger prompt. You see x -changing in the Variable Explorer. You should also see ``x`` changing -when its value is printed as part of the ``demo()`` function. (The -printed output appears between your debugger commands and responses.) - -This debugging ability to execute code line by line, to inspect variables as -they change, and to modify them manually is a powerful tool to -understand what a piece of code is doing (and to correct it if desired). - -To leave the debugging mode, you can type ``exit`` or select from the -menu ``Debug > Debugging Control > Exit`` - -Debugging once an exception has occurred with IPython ------------------------------------------------------ - -In the IPython console, we can call ``%debug`` -straight after an exception has been raised: this will start the -IPython debug mode, which allows inspection of local variables at the -point where the exception occurred as described above. This is a lot -more efficient than adding ``print`` statements to the code an -running it again. - -If you use this, you may also want to use the commands ``up`` -(i.e. press ``u`` at the debugger) and ``down`` (i.e. press ``d``) which -navigate the inspection point up and down the stack. (Up the stack means -to the functions that have called the current function; down is the -opposite direction.) - - - -Plotting -######## - -Plotting with the IPython console ---------------------------------- - -Assuming we use an IPython console with version >= 1.0.0, we can -decide whether figures created with matplotlib/pylab will show - -1. *inline*, i.e. inside the IPython console, or whether they should - -2. appear inside a new window. - -Option 1 is convenient to save a record of the interactive session -(section `Shortcuts for useful functions`_ lists a shortcut to save -the IPython console to an html file). - -Option 2 allows to interactively zoom into the figure, manipulate it a little, -and save the figure to different file formats via a menu the window it -contains has. - -The command to get the figures to appear *inline* in the IPython -console is:: - - In [3]: %matplotlib inline - -The command to get figures appear in their own window (which -technically is a Qt windown) is:: - - In [4]: %matplotlib qt - -The Spyder preferences can be used to customize the default behavior -(in particular ``Preferences > IPython Console > Graphics > -Activate Support`` to switch into inline plotting). - -Here are two lines you can use to quickly create a plot and test -this:: - - In [5]: import pylab - In [6]: pylab.plot(range(10), 'o') - - -Plotting with the Python console --------------------------------- - -If we use the Python console, all plots will appear in a new window -(there is no way of making it appear inline inside the Python -console - this only works for the IPython Console). - -Here is a brief example that you can use to create and display a -plot:: - - >>> import pylab - >>> pylab.plot(range(10), 'o') - -If you execute your code in a dedicated console, you need to use -matplotlib's or pylab's ``show()`` command in your code to make a plot -appear, like this: ``pylab.show()``. - -Note that the ``show()`` command will bind the focus to new window -that has appeared, and that you will need to close that window before -Spyder can accept any further commands or respond to interaction. If -you cannot see the new window, check whether it may have appeared behind -the Spyder window, or be partly hidden. - - - -Historical note -############### - -This tutorial is based on `notes -`__ -by `Hans Fangohr `__, that are -used at the `University of Southampton `__ to -`teach Python for computational modelling -`__ to -undegraduate engineers and postgraduate PhD students for the -`Next Generation Computational Modelling `__ -doctoral training centre. diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/utils/introspection/fallback_plugin.py spyder-3.0.2+dfsg1/spyder/utils/introspection/fallback_plugin.py --- spyder-2.3.8+dfsg1/spyder/utils/introspection/fallback_plugin.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/utils/introspection/fallback_plugin.py 2016-10-25 02:05:23.000000000 +0200 @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- # -# Copyright © 2013 The Spyder Development Team +# Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) +# (see spyder/__init__.py for details) """ Introspection utilities used by Spyder @@ -15,11 +15,14 @@ import re import time -from spyderlib.utils.debug import log_dt -from spyderlib.utils import sourcecode, encoding -from spyderlib.utils.introspection.module_completion import module_completion -from spyderlib.utils.introspection.plugin_manager import ( - DEBUG_EDITOR, LOG_FILENAME, IntrospectionPlugin, memoize) +from pygments.token import Token + +from spyder.utils.debug import log_dt +from spyder.utils import sourcecode, encoding +from spyder.utils.introspection.manager import ( + DEBUG_EDITOR, LOG_FILENAME, IntrospectionPlugin) +from spyder.utils.introspection.utils import ( + get_parent_until, memoize, find_lexer_for_filename, get_keywords) class FallbackPlugin(IntrospectionPlugin): @@ -29,32 +32,48 @@ name = 'fallback' def get_completions(self, info): - """Return a list of completion strings + """Return a list of (completion, type) tuples Simple completion based on python-like identifiers and whitespace """ + if not info['obj']: + return items = [] - if (info.line.strip().startswith(('import ', 'from ')) and - info.is_python_like): - items += module_completion(info.line, [info.filename]) - elif info.obj: - base = info.obj - tokens = set(info.split_words(-1)) + obj = info['obj'] + if info['context']: + lexer = find_lexer_for_filename(info['filename']) + # get a list of token matches for the current object + tokens = lexer.get_tokens(info['source_code']) + for (context, token) in tokens: + token = token.strip() + if (context in info['context'] and + token.startswith(obj) and + obj != token): + items.append(token) + # add in keywords if not in a string + if context not in Token.Literal.String: + try: + keywords = get_keywords(lexer) + items.extend(k for k in keywords if k.startswith(obj)) + except Exception: + pass + else: + tokens = set(re.findall(info['id_regex'], info['source_code'])) items = [item for item in tokens if - item.startswith(base) and len(item) > len(base)] - if '.' in base: - start = base.rfind('.') + 1 + item.startswith(obj) and len(item) > len(obj)] + if '.' in obj: + start = obj.rfind('.') + 1 else: start = 0 - - items = [i[start:len(base)] + i[len(base):].split('.')[0] - for i in items] - # get path completions - # get last word back to a space or a quote character - match = re.search('''[ "\']([\w\.\\\\/]+)\Z''', info.line) - if match: - items += _complete_path(match.groups()[0]) - return list(sorted(items)) + + items = [i[start:len(obj)] + i[len(obj):].split('.')[0] + for i in items] + # get path completions + # get last word back to a space or a quote character + match = re.search('''[ "\']([\w\.\\\\/]+)\Z''', info['line']) + if match: + items += _complete_path(match.groups()[0]) + return [(i, '') for i in sorted(items)] def get_definition(self, info): """ @@ -63,12 +82,16 @@ This is used to find the path of python-like modules (e.g. cython and enaml) for a goto definition """ - token = info.obj - lines = info.lines - source_code = info.source_code - filename = info.filename + if not info['is_python_like']: + return + token = info['obj'] + lines = info['lines'] + source_code = info['source_code'] + filename = info['filename'] line_nr = None + if token is None: + return if '.' in token: token = token.split('.')[-1] @@ -76,7 +99,7 @@ len(lines)) if line_nr is None: return - line = info.line + line = info['line'] exts = python_like_exts() if not osp.splitext(filename)[-1] in exts: return filename, line_nr @@ -87,7 +110,7 @@ if (not source_file or not osp.splitext(source_file)[-1] in exts): line_nr = get_definition_with_regex(source_code, token, - line_nr) + line_nr) return filename, line_nr mod_name = osp.basename(source_file).split('.')[0] if mod_name == token or mod_name == '__init__': @@ -100,6 +123,21 @@ return filename, line_nr + def get_info(self, info): + """Get a formatted calltip and docstring from Fallback""" + if info['docstring']: + if info['filename']: + filename = os.path.basename(info['filename']) + filename = os.path.splitext(filename)[0] + else: + filename = '' + resp = dict(docstring=info['docstring'], + name=filename, + note='', + argspec='', + calltip=None) + return resp + @memoize def python_like_mod_finder(import_line, alt_path=None, @@ -184,7 +222,6 @@ 'self.{0}{1}[^=!<>]*=[^=]', '{0}{1}[^=!<>]*=[^=]'] matches = get_matches(patterns, source, token, start_line) - # find the one closest to the start line (prefer before the start line) if matches: min_dist = len(source.splitlines()) @@ -271,7 +308,7 @@ if __name__ == '__main__': - from spyderlib.utils.introspection.plugin_manager import CodeInfo + from spyder.utils.introspection.manager import CodeInfo p = FallbackPlugin() @@ -280,25 +317,25 @@ code += '\nlog_dt' path, line = p.get_definition(CodeInfo('definition', code, len(code), - __file__)) + __file__, is_python_like=True)) assert path.endswith('fallback_plugin.py') code += '\np.get_completions' path, line = p.get_definition(CodeInfo('definition', code, len(code), - 'dummy.txt')) - assert path == 'dummy.txt' + 'dummy.py', is_python_like=True)) + assert path == 'dummy.py' assert 'def get_completions(' in code.splitlines()[line - 1] code += '\npython_like_mod_finder' path, line = p.get_definition(CodeInfo('definition', code, len(code), - 'dummy.txt')) - assert path == 'dummy.txt' + 'dummy.py', is_python_like=True)) + assert path == 'dummy.py' # FIXME: we need to prioritize def over = assert 'def python_like_mod_finder' in code.splitlines()[line - 1] code += 'python_like_mod_finder' resp = p.get_definition(CodeInfo('definition', code, len(code), - 'dummy.txt')) + 'dummy.py')) assert resp is None code = """ @@ -309,60 +346,54 @@ t = Test() t.foo""" path, line = p.get_definition(CodeInfo('definition', code, len(code), - 'dummy.txt')) + 'dummy.py', is_python_like=True)) assert line == 4 ext = python_like_exts() assert '.py' in ext and '.pyx' in ext ext = all_editable_exts() - assert '.cfg' in ext and '.iss' in ext + assert '.cpp' in ext and '.html' in ext - path = p.get_parent_until(os.path.abspath(__file__)) - assert path == 'spyderlib.utils.introspection.fallback_plugin' + path = get_parent_until(os.path.abspath(__file__)) + assert path == 'spyder.utils.introspection.fallback_plugin' - line = 'from spyderlib.widgets.sourcecode.codeeditor import CodeEditor' + line = 'from spyder.widgets.sourcecode.codeeditor import CodeEditor' path = python_like_mod_finder(line) assert path.endswith('codeeditor.py') path = python_like_mod_finder(line, stop_token='sourcecode') assert path.endswith('__init__.py') and 'sourcecode' in path - path = p.get_parent_until(osp.expanduser(r'~/.spyder2/temp.py')) - assert path == '.spyder2.temp' + path = osp.expanduser(r'~/.spyder2/temp.py') + if os.path.exists(path): + path = get_parent_until(path) + assert path == '.spyder2.temp', path code = 'import re\n\nre' path, line = p.get_definition(CodeInfo('definition', code, len(code), - 'dummy.txt')) - assert path == 'dummy.txt' and line == 1 + 'dummy.py', is_python_like=True)) + assert path == 'dummy.py' and line == 1 code = 'self.proxy.widget; self.p' - comp = p.get_completions(CodeInfo('completions', code, len(code))) - assert comp == ['proxy'] + comp = p.get_completions(CodeInfo('completions', code, len(code), 'dummy.py')) + assert ('proxy', '') in comp, comp code = 'self.sigMessageReady.emit; self.s' - comp = p.get_completions(CodeInfo('completions', code, len(code))) - assert comp == ['sigMessageReady'] + comp = p.get_completions(CodeInfo('completions', code, len(code), 'dummy.py')) + assert ('sigMessageReady', '') in comp - code = encoding.to_unicode('álfa;á') - comp = p.get_completions(CodeInfo('completions', code, len(code))) - assert comp == [encoding.to_unicode('álfa')] - - code = 'from numpy import one' - comp = p.get_completions(CodeInfo('completions', code, len(code))) - assert 'ones' in comp - - comp = p.get_completions(CodeInfo('completions', code, len(code), - is_python_like=False)) - assert not comp - - code = 'from numpy.testing import (asse' - comp = p.get_completions(CodeInfo('completions', code, len(code))) - assert 'assert_equal' in comp + code = 'bob = 1; bo' + comp = p.get_completions(CodeInfo('completions', code, len(code), 'dummy.m')) + assert ('bob', '') in comp + + code = 'functi' + comp = p.get_completions(CodeInfo('completions', code, len(code), 'dummy.sh')) + assert ('function', '') in comp, comp code = ''' def test(a, b): pass test(1,''' path, line = p.get_definition(CodeInfo('definition', code, len(code), - 'dummy.txt')) + 'dummy.py', is_python_like=True)) assert line == 2 diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/utils/introspection/__init__.py spyder-3.0.2+dfsg1/spyder/utils/introspection/__init__.py --- spyder-2.3.8+dfsg1/spyder/utils/introspection/__init__.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/utils/introspection/__init__.py 2016-10-25 02:05:23.000000000 +0200 @@ -1,11 +1,9 @@ # -*- coding: utf-8 -*- # -# Copyright © 2013 The Spyder Development Team +# Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) +# (see spyder/__init__.py for details) """ Introspection utilities used by Spyder """ -from . import module_completion -from .plugin_manager import PluginManager diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/utils/introspection/jedi_plugin.py spyder-3.0.2+dfsg1/spyder/utils/introspection/jedi_plugin.py --- spyder-2.3.8+dfsg1/spyder/utils/introspection/jedi_plugin.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/utils/introspection/jedi_plugin.py 2016-10-25 02:05:23.000000000 +0200 @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- # -# Copyright © 2013 The Spyder Development Team +# Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) +# (see spyder/__init__.py for details) """ Jedi Introspection Plugin @@ -11,15 +11,15 @@ import os.path as osp import sys import time -import threading -from spyderlib import dependencies -from spyderlib.baseconfig import _, debug_print -from spyderlib.utils import programs -from spyderlib.utils.debug import log_last_error, log_dt -from spyderlib.utils.dochelpers import getsignaturefromtext -from spyderlib.utils.introspection.plugin_manager import ( +from spyder.config.base import debug_print +from spyder.utils import programs +from spyder.utils.debug import log_last_error, log_dt +from spyder.utils.dochelpers import getsignaturefromtext +from spyder.utils.introspection.manager import ( DEBUG_EDITOR, LOG_FILENAME, IntrospectionPlugin) +from spyder.utils.introspection.utils import get_parent_until +from spyder.utils.introspection.manager import JEDI_REQVER try: import jedi @@ -27,13 +27,6 @@ jedi = None -JEDI_REQVER = '>=0.8.1;<0.9.0' -dependencies.add('jedi', - _("(Experimental) Editor's code completion," - " go-to-definition and help"), - required_version=JEDI_REQVER) - - class JediPlugin(IntrospectionPlugin): """ Jedi based introspection plugin for jedi @@ -49,15 +42,15 @@ if not programs.is_module_installed('jedi', JEDI_REQVER): raise ImportError('Requires Jedi %s' % JEDI_REQVER) jedi.settings.case_insensitive_completion = False - self.busy = True - self._warmup_thread = threading.Thread(target=self.preload) - self._warmup_thread.start() + for lib in ['numpy', 'matplotlib']: + jedi.preload_module(lib) def get_completions(self, info): - """Return a list of completion strings""" + """Return a list of (completion, type) tuples""" completions = self.get_jedi_object('completions', info) + completions = [(c.name, c.type) for c in completions] debug_print(str(completions)[:100]) - return [c.name for c in completions] + return completions def get_info(self, info): """ @@ -81,7 +74,7 @@ if name is None: return if call_def.module_path: - mod_name = self.get_parent_until(call_def.module_path) + mod_name = get_parent_until(call_def.module_path) else: mod_name = None if not mod_name: @@ -128,9 +121,10 @@ module. Falls back on token lookup if it is in an enaml file or does not find a match """ - line, filename = info.line_num, info.filename + line, filename = info['line_num'], info['filename'] def_info, module_path, line_nr = None, None, None gotos = self.get_jedi_object('goto_assignments', info) + if gotos: def_info = self.get_definition_info(gotos[0]) if def_info and def_info['goto_next']: @@ -151,10 +145,6 @@ return return module_path, line_nr - def set_pref(self, name, value): - """Set a plugin preference to a value""" - pass - # ---- Private API ------------------------------------------------------- def get_jedi_object(self, func_name, info, use_filename=True): @@ -171,13 +161,13 @@ sys.meta_path.remove(meta) if use_filename: - filename = info.filename + filename = info['filename'] else: filename = None try: - script = jedi.Script(info.source_code, info.line_num, - info.column, filename) + script = jedi.Script(info['source_code'], info['line_num'], + info['column'], filename) func = getattr(script, func_name) val = func() except Exception as e: @@ -199,7 +189,10 @@ try: module_path = defn.module_path name = defn.name - line_nr = defn.line_nr + if hasattr(defn, 'line_nr'): + line_nr = defn.line_nr + else: + line_nr = defn.line description = defn.description in_builtin = defn.in_builtin_module() except Exception as e: @@ -240,25 +233,13 @@ line_nr = None return module_path, line_nr - def preload(self): - """Preload a list of libraries""" - for lib in ['numpy']: - jedi.preload_module(lib) - self.busy = False - if __name__ == '__main__': - from spyderlib.utils.introspection.plugin_manager import CodeInfo + from spyder.utils.introspection.manager import CodeInfo p = JediPlugin() p.load_plugin() - print('Warming up Jedi') - t0 = time.time() - while p.busy: - time.sleep(0.1) - print('Warmed up in %0.1f s' % (time.time() - t0)) - source_code = "import numpy; numpy.ones(" docs = p.get_info(CodeInfo('info', source_code, len(source_code))) @@ -267,17 +248,17 @@ source_code = "import n" completions = p.get_completions(CodeInfo('completions', source_code, len(source_code))) - assert 'numpy' in completions + assert ('numpy', 'module') in completions - source_code = "import matplotlib.pyplot as plt; plt.imsave" + source_code = "import pandas as pd; pd.DataFrame" path, line_nr = p.get_definition(CodeInfo('definition', source_code, len(source_code))) - assert 'pyplot.py' in path + assert 'frame.py' in path - source_code = 'from .plugin_manager import memoize' + source_code = 'from .utils import CodeInfo' path, line_nr = p.get_definition(CodeInfo('definition', source_code, len(source_code), __file__)) - assert 'plugin_manager.py' in path and 'introspection' in path + assert 'utils.py' in path and 'introspection' in path code = ''' def test(a, b): @@ -285,7 +266,7 @@ pass test(1,''' path, line = p.get_definition(CodeInfo('definition', code, len(code), - 'dummy.txt')) + 'dummy.txt', is_python_like=True)) assert line == 2 docs = p.get_info(CodeInfo('info', code, len(code), __file__)) diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/utils/introspection/manager.py spyder-3.0.2+dfsg1/spyder/utils/introspection/manager.py --- spyder-2.3.8+dfsg1/spyder/utils/introspection/manager.py 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/utils/introspection/manager.py 2016-10-25 02:05:23.000000000 +0200 @@ -0,0 +1,354 @@ +# -*- coding: utf-8 -*- +# +# Copyright © Spyder Project Contributors +# Licensed under the terms of the MIT License +# (see spyder/__init__.py for details) + +# Standard library imports +from __future__ import print_function +from collections import OrderedDict +import time + +# Third party imports +from qtpy.QtCore import QObject, QTimer, Signal +from qtpy.QtWidgets import QApplication + +# Local imports +from spyder import dependencies +from spyder.config.base import _, DEBUG, debug_print, get_conf_path +from spyder.utils import sourcecode +from spyder.utils.introspection.plugin_client import PluginClient +from spyder.utils.introspection.utils import CodeInfo + + +PLUGINS = ['rope', 'jedi', 'fallback'] + +LOG_FILENAME = get_conf_path('introspection.log') +DEBUG_EDITOR = DEBUG >= 3 +LEAD_TIME_SEC = 0.25 + + +ROPE_REQVER = '>=0.9.4' +dependencies.add('rope', + _("Editor's code completion, go-to-definition and help"), + required_version=ROPE_REQVER) + +JEDI_REQVER = '>=0.8.1' +dependencies.add('jedi', + _("Editor's code completion, go-to-definition and help"), + required_version=JEDI_REQVER) + + +class PluginManager(QObject): + + introspection_complete = Signal(object) + + def __init__(self, executable): + super(PluginManager, self).__init__() + plugins = OrderedDict() + for name in PLUGINS: + try: + plugin = PluginClient(name, executable) + plugin.run() + except Exception as e: + debug_print('Introspection Plugin Failed: %s' % name) + debug_print(str(e)) + continue + debug_print('Introspection Plugin Loaded: %s' % name) + plugins[name] = plugin + plugin.received.connect(self.handle_response) + self.plugins = plugins + self.timer = QTimer() + self.desired = [] + self.ids = dict() + self.info = None + self.request = None + self.pending = None + self.pending_request = None + self.waiting = False + + def send_request(self, info): + """Handle an incoming request from the user.""" + if self.waiting: + if info.serialize() != self.info.serialize(): + self.pending_request = info + else: + debug_print('skipping duplicate request') + return + debug_print('%s request' % info.name) + desired = None + self.info = info + editor = info.editor + if (info.name == 'completion' and 'jedi' not in self.plugins and + info.line.lstrip().startswith(('import ', 'from '))): + desired = 'fallback' + + if ((not editor.is_python_like()) or + sourcecode.is_keyword(info.obj) or + (editor.in_comment_or_string() and info.name != 'info')): + desired = 'fallback' + + plugins = self.plugins.values() + if desired: + plugins = [self.plugins[desired]] + self.desired = [desired] + elif (info.name == 'definition' and not info.editor.is_python() or + info.name == 'info'): + self.desired = list(self.plugins.keys()) + else: + # Use all but the fallback + plugins = list(self.plugins.values())[:-1] + self.desired = list(self.plugins.keys())[:-1] + + self._start_time = time.time() + self.waiting = True + method = 'get_%s' % info.name + value = info.serialize() + self.ids = dict() + for plugin in plugins: + request_id = plugin.request(method, value) + self.ids[request_id] = plugin.name + self.timer.stop() + self.timer.singleShot(LEAD_TIME_SEC * 1000, self._handle_timeout) + + def validate(self): + for plugin in self.plugins.values(): + plugin.request('validate') + + def handle_response(self, response): + name = self.ids.get(response['request_id'], None) + if not name: + return + if response.get('error', None): + debug_print('Response error:', response['error']) + return + if name == self.desired[0] or not self.waiting: + if response.get('result', None): + self._finalize(response) + else: + self.pending = response + + def close(self): + [plugin.close() for plugin in self.plugins] + + def _finalize(self, response): + self.waiting = False + self.pending = None + if self.info: + delta = time.time() - self._start_time + debug_print('%s request from %s finished: "%s" in %.1f sec' + % (self.info.name, response['name'], + str(response['result'])[:100], delta)) + response['info'] = self.info + self.introspection_complete.emit(response) + self.info = None + if self.pending_request: + info = self.pending_request + self.pending_request = None + self.send_request(info) + + def _handle_timeout(self): + self.waiting = False + if self.pending: + self._finalize(self.pending) + else: + debug_print('No valid responses acquired') + + +class IntrospectionManager(QObject): + + send_to_help = Signal(str, str, str, str, bool) + edit_goto = Signal(str, int, str) + + def __init__(self, executable=None): + super(IntrospectionManager, self).__init__() + self.editor_widget = None + self.pending = None + self.plugin_manager = PluginManager(executable) + self.plugin_manager.introspection_complete.connect( + self._introspection_complete) + + def change_executable(self, executable): + self.plugin_manager.close() + self.plugin_manager = PluginManager(executable) + self.plugin_manager.introspection_complete.connect( + self._introspection_complete) + + def set_editor_widget(self, editor_widget): + self.editor_widget = editor_widget + + def _get_code_info(self, name, position=None, **kwargs): + + editor = self.editor_widget.get_current_editor() + finfo = self.editor_widget.get_current_finfo() + in_comment_or_string = editor.in_comment_or_string() + + if position is None: + position = editor.get_position('cursor') + + kwargs['editor'] = editor + kwargs['finfo'] = finfo + kwargs['editor_widget'] = self.editor_widget + + return CodeInfo(name, finfo.get_source_code(), position, + finfo.filename, editor.is_python_like, in_comment_or_string, + **kwargs) + + def get_completions(self, automatic): + """Get code completion""" + info = self._get_code_info('completions', automatic=automatic) + self.plugin_manager.send_request(info) + + def go_to_definition(self, position): + """Go to definition""" + info = self._get_code_info('definition', position) + self.plugin_manager.send_request(info) + + def show_object_info(self, position, auto=True): + """Show signature calltip and/or docstring in the Help plugin""" + # auto is True means that this method was called automatically, + # i.e. the user has just entered an opening parenthesis -- in that + # case, we don't want to force Help to be visible, to avoid polluting + # the window layout + info = self._get_code_info('info', position, auto=auto) + self.plugin_manager.send_request(info) + + def validate(self): + """Validate the plugins""" + self.plugin_manager.validate() + + def is_editor_ready(self): + """Check if the main app is starting up""" + if self.editor_widget: + window = self.editor_widget.window() + if hasattr(window, 'is_starting_up') and not window.is_starting_up: + return True + + def _introspection_complete(self, response): + """ + Handle an introspection response completion. + + Route the response to the correct handler. + """ + result = response.get('result', None) + if result is None: + return + info = response['info'] + current = self._get_code_info(response['info']['name']) + + if result and current.filename == info.filename: + func = getattr(self, '_handle_%s_result' % info.name) + try: + func(result, current, info) + except Exception as e: + debug_print(e) + + def _handle_completions_result(self, comp_list, info, prev_info): + """ + Handle a `completions` result. + + Only handle the response if we are on the same line of text and + on the same `obj` as the original request. + """ + if info.line_num != prev_info.line_num: + return + completion_text = info.obj + prev_text = prev_info.obj + + if prev_info.obj is None: + completion_text = '' + prev_text = '' + + if not completion_text.startswith(prev_text): + return + + if info.full_obj and len(info.full_obj) > len(info.obj): + new_list = [(c, t) for (c, t) in comp_list + if c.startswith(info.full_obj)] + if new_list: + pos = info.editor.get_position('cursor') + new_pos = pos + len(info.full_obj) - len(info.obj) + info.editor.set_cursor_position(new_pos) + completion_text = info.full_obj + comp_list = new_list + + if '.' in completion_text: + completion_text = completion_text.split('.')[-1] + + comp_list = [(c.split('.')[-1], t) for (c, t) in comp_list] + comp_list = [(c, t) for (c, t) in comp_list + if c.startswith(completion_text)] + + info.editor.show_completion_list(comp_list, completion_text, + prev_info.automatic) + + def _handle_info_result(self, resp, info, prev_info): + """ + Handle an `info` result, triggering a calltip and/or docstring. + + Only handle the response if we are on the same line of text as + when the request was initiated. + """ + if info.line_num != prev_info.line_num: + return + + if resp['calltip']: + info.editor.show_calltip('Arguments', resp['calltip'], + signature=True, + at_position=prev_info.position) + + if resp['name']: + self.send_to_help.emit( + resp['name'], resp['argspec'], + resp['note'], resp['docstring'], + not prev_info.auto) + + def _handle_definition_result(self, resp, info, prev_info): + """Handle a `definition` result""" + fname, lineno = resp + self.edit_goto.emit(fname, lineno, "") + + def _post_message(self, message, timeout=60000): + """ + Post a message to the main window status bar with a timeout in ms + """ + if self.editor_widget: + try: + statusbar = self.editor_widget.window().statusBar() + statusbar.showMessage(message, timeout) + QApplication.processEvents() + except AttributeError: + pass + + +class IntrospectionPlugin(object): + + def load_plugin(self): + """Initialize the plugin""" + pass + + def get_completions(self, info): + """Get a list of completions""" + pass + + def get_info(self, info): + """ + Find the calltip and docs + + Returns a dict like the following: + {'note': 'Function of numpy.core.numeric...', + 'argspec': "(shape, dtype=None, order='C')' + 'docstring': 'Return an array of given...' + 'name': 'ones', + 'calltip': 'ones(shape, dtype=None, order='C')'} + """ + pass + + def get_definition(self, info): + """Get a (filename, line_num) location for a definition""" + pass + + def validate(self): + """Validate the plugin""" + pass + diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/utils/introspection/module_completion.py spyder-3.0.2+dfsg1/spyder/utils/introspection/module_completion.py --- spyder-2.3.8+dfsg1/spyder/utils/introspection/module_completion.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/utils/introspection/module_completion.py 2016-10-25 02:05:23.000000000 +0200 @@ -14,7 +14,7 @@ # # Distributed under the terms of the BSD License. # Copyright (C) 2010-2011 The IPython Development Team. -# Copyright (C) 2013 The Spyder Development Team +# Copyright (C) Spyder Project Contributors # #------------------------------------------------------------------------------ @@ -27,8 +27,10 @@ import sys from zipimport import zipimporter -from spyderlib.baseconfig import get_conf_path, running_in_mac_app -from spyderlib.utils.external.pickleshare import PickleShareDB +from spyder.config.base import get_conf_path, running_in_mac_app +from spyder.py3compat import PY3 + +from pickleshare import PickleShareDB #----------------------------------------------------------------------------- # Globals and constants @@ -38,7 +40,10 @@ MODULES_PATH = get_conf_path('db') # Time in seconds after which we give up -TIMEOUT_GIVEUP = 20 +if os.name == 'nt': + TIMEOUT_GIVEUP = 30 +else: + TIMEOUT_GIVEUP = 20 # Py2app only uses .pyc files for the stdlib when optimize=0, # so we need to add it as another suffix here @@ -281,6 +286,7 @@ 'PyQt4', 'PySide', 'os.path'] submodules = [] + for m in mods: submods = get_submodules(m) submodules += submods @@ -314,9 +320,15 @@ s = 'from xml.etree.ElementTree import ' assert module_completion(s + 'V') == ['VERSION'] - assert sorted(module_completion(s + 'VERSION, XM')) == \ - ['XML', 'XMLID', 'XMLParser', 'XMLTreeBuilder'] + if PY3: + assert sorted(module_completion(s + 'VERSION, XM')) == \ + ['XML', 'XMLID', 'XMLParser', 'XMLPullParser'] + else: + assert sorted(module_completion(s + 'VERSION, XM')) == \ + ['XML', 'XMLID', 'XMLParser', 'XMLTreeBuilder'] assert module_completion(s + '(dum') == ['dump'] assert module_completion(s + '(dump, Su') == ['SubElement'] + + assert 'os.path' in get_preferred_submodules() diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/utils/introspection/plugin_client.py spyder-3.0.2+dfsg1/spyder/utils/introspection/plugin_client.py --- spyder-2.3.8+dfsg1/spyder/utils/introspection/plugin_client.py 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/utils/introspection/plugin_client.py 2016-10-25 02:05:23.000000000 +0200 @@ -0,0 +1,227 @@ +# -*- coding: utf-8 -*- +# +# Copyright © Spyder Project Contributors +# Licensed under the terms of the MIT License +# (see spyder/__init__.py for details) + +# Local imports +import imp +import os +import os.path as osp +import sys +import uuid + +# Third party imports +from qtpy.QtCore import (QObject, QProcess, QProcessEnvironment, + QSocketNotifier, QTimer, Signal) +from qtpy.QtWidgets import QApplication +import zmq + +# Local imports +from spyder.config.base import debug_print, DEV, get_module_path + + +# Heartbeat timer in milliseconds +HEARTBEAT = 1000 + + +class AsyncClient(QObject): + + """ + A class which handles a connection to a client through a QProcess. + """ + + # Emitted when the client has initialized. + initialized = Signal() + + # Emitted when the client errors. + errored = Signal() + + # Emitted when a request response is received. + received = Signal(object) + + def __init__(self, target, executable=None, name=None, + extra_args=None, libs=None, cwd=None, env=None): + super(AsyncClient, self).__init__() + self.executable = executable or sys.executable + self.extra_args = extra_args + self.target = target + self.name = name or self + self.libs = libs + self.cwd = cwd + self.env = env + self.is_initialized = False + self.closing = False + self.context = zmq.Context() + QApplication.instance().aboutToQuit.connect(self.close) + + # Set up the heartbeat timer. + self.timer = QTimer(self) + self.timer.timeout.connect(self._heartbeat) + + def run(self): + """Handle the connection with the server. + """ + # Set up the zmq port. + self.socket = self.context.socket(zmq.PAIR) + self.port = self.socket.bind_to_random_port('tcp://*') + + # Set up the process. + self.process = QProcess(self) + if self.cwd: + self.process.setWorkingDirectory(self.cwd) + p_args = ['-u', self.target, str(self.port)] + if self.extra_args is not None: + p_args += self.extra_args + + # Set up environment variables. + processEnvironment = QProcessEnvironment() + env = self.process.systemEnvironment() + if (self.env and 'PYTHONPATH' not in self.env) or DEV: + python_path = osp.dirname(get_module_path('spyder')) + # Add the libs to the python path. + for lib in self.libs: + try: + path = osp.dirname(imp.find_module(lib)[1]) + python_path = osp.pathsep.join([python_path, path]) + except ImportError: + pass + env.append("PYTHONPATH=%s" % python_path) + if self.env: + env.update(self.env) + for envItem in env: + envName, separator, envValue = envItem.partition('=') + processEnvironment.insert(envName, envValue) + self.process.setProcessEnvironment(processEnvironment) + + # Start the process and wait for started. + self.process.start(self.executable, p_args) + self.process.finished.connect(self._on_finished) + running = self.process.waitForStarted() + if not running: + raise IOError('Could not start %s' % self) + + # Set up the socket notifer. + fid = self.socket.getsockopt(zmq.FD) + self.notifier = QSocketNotifier(fid, QSocketNotifier.Read, self) + self.notifier.activated.connect(self._on_msg_received) + + def request(self, func_name, *args, **kwargs): + """Send a request to the server. + + The response will be a dictionary the 'request_id' and the + 'func_name' as well as a 'result' field with the object returned by + the function call or or an 'error' field with a traceback. + """ + if not self.is_initialized: + return + request_id = uuid.uuid4().hex + request = dict(func_name=func_name, + args=args, + kwargs=kwargs, + request_id=request_id) + self._send(request) + return request_id + + def close(self): + """Cleanly close the connection to the server. + """ + self.closing = True + self.is_initialized = False + self.timer.stop() + self.notifier.activated.disconnect(self._on_msg_received) + self.notifier.setEnabled(False) + del self.notifier + self.request('server_quit') + self.process.waitForFinished(1000) + self.process.close() + self.context.destroy() + + def _on_finished(self): + """Handle a finished signal from the process. + """ + if self.closing: + return + if self.is_initialized: + debug_print('Restarting %s' % self.name) + debug_print(self.process.readAllStandardOutput()) + debug_print(self.process.readAllStandardError()) + self.is_initialized = False + self.notifier.setEnabled(False) + self.run() + else: + debug_print('Errored %s' % self.name) + debug_print(self.process.readAllStandardOutput()) + debug_print(self.process.readAllStandardError()) + self.errored.emit() + + def _on_msg_received(self): + """Handle a message trigger from the socket. + """ + self.notifier.setEnabled(False) + while 1: + try: + resp = self.socket.recv_pyobj(flags=zmq.NOBLOCK) + except zmq.ZMQError: + self.notifier.setEnabled(True) + return + if not self.is_initialized: + self.is_initialized = True + debug_print('Initialized %s' % self.name) + self.initialized.emit() + self.timer.start(HEARTBEAT) + continue + resp['name'] = self.name + self.received.emit(resp) + + def _heartbeat(self): + """Send a heartbeat to keep the server alive. + """ + self._send(dict(func_name='server_heartbeat')) + + def _send(self, obj): + """Send an object to the server. + """ + try: + self.socket.send_pyobj(obj, zmq.NOBLOCK) + except Exception as e: + debug_print(e) + self.is_initialized = False + self._on_finished() + + +class PluginClient(AsyncClient): + + def __init__(self, plugin_name, executable=None, env=None): + cwd = os.path.dirname(__file__) + super(PluginClient, self).__init__('plugin_server.py', + executable=executable, cwd=cwd, env=env, + extra_args=[plugin_name], libs=[plugin_name]) + self.name = plugin_name + + +if __name__ == '__main__': + app = QApplication(sys.argv) + plugin = PluginClient('jedi') + plugin.run() + + def handle_return(value): + print(value) + if value['func_name'] == 'foo': + app.quit() + else: + plugin.request('foo') + + def handle_errored(): + print('errored') + sys.exit(1) + + def start(): + print('start') + plugin.request('validate') + + plugin.errored.connect(handle_errored) + plugin.received.connect(handle_return) + plugin.initialized.connect(start) + + app.exec_() diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/utils/introspection/plugin_manager.py spyder-3.0.2+dfsg1/spyder/utils/introspection/plugin_manager.py --- spyder-2.3.8+dfsg1/spyder/utils/introspection/plugin_manager.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/utils/introspection/plugin_manager.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,530 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright © 2015 The Spyder development team -# Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) - -from __future__ import print_function - -import re -from collections import OrderedDict -import functools -import os.path as osp -import os -import imp -import time - -from spyderlib.baseconfig import DEBUG, get_conf_path, debug_print -from spyderlib.utils.introspection.module_completion import ( - get_preferred_submodules) -from spyderlib.utils import sourcecode -from spyderlib.utils.debug import log_last_error - -from spyderlib.qt.QtGui import QApplication -from spyderlib.qt.QtCore import Signal, QThread, QObject, QTimer - - -PLUGINS = ['rope', 'jedi', 'fallback'] - -LOG_FILENAME = get_conf_path('introspection.log') -DEBUG_EDITOR = DEBUG >= 3 -LEAD_TIME_SEC = 0.25 - - -class RequestHandler(QObject): - - """Handle introspection request. - """ - - introspection_complete = Signal() - - def __init__(self, code_info, plugins): - super(RequestHandler, self).__init__() - self.info = code_info - self.timer = QTimer() - self.timer.singleShot(LEAD_TIME_SEC * 1000, self._handle_timeout) - self.waiting = True - self.pending = {} - self.result = None - self.plugins = plugins - self._start_time = time.time() - self._threads = {} - for plugin in plugins: - self._make_async_call(plugin, code_info) - - def _handle_timeout(self): - debug_print('got timeout') - if self.pending: - for plugin in self.plugins: - if plugin.name in self.pending: - self._finalize(plugin.name, self.pending[plugin.name]) - return - self.waiting = False - - def _handle_incoming(self, name): - # coerce to a str in case it is a QString - name = str(name) - self._threads[name].wait() - if self.result: - return - result = self._threads[name].result - if name == self.plugins[0].name or not self.waiting: - self._finalize(name, result) - else: - self.pending[name] = result - - def _make_async_call(self, plugin, info): - """Trigger an introspection job in a thread""" - self._threads[str(plugin.name)] = thread = IntrospectionThread(plugin, info) - thread.request_handled.connect(self._handle_incoming) - thread.start() - - def _finalize(self, name, result): - self.result = result - self.waiting = False - self.pending = None - delta = time.time() - self._start_time - debug_print('%s request from %s complete: "%s" in %.1f sec' - % (self.info.name, name, str(result)[:100], delta)) - self.introspection_complete.emit() - - -class GetSubmodulesThread(QThread): - - """ - A thread to generate a list of submodules to be passed to - introspection plugins - """ - - def __init__(self): - super(GetSubmodulesThread, self).__init__() - self.submods = [] - - def run(self): - self.submods = get_preferred_submodules() - - -class IntrospectionThread(QThread): - - """ - A thread to perform an introspection task - """ - - request_handled = Signal(str) - - def __init__(self, plugin, info): - super(IntrospectionThread, self).__init__() - self.plugin = plugin - self.info = info - self.result = None - - def run(self): - func = getattr(self.plugin, 'get_%s' % self.info.name) - self.plugin.busy = True - try: - self.result = func(self.info) - except Exception as e: - debug_print(e) - self.plugin.busy = False - self.request_handled.emit(self.plugin.name) - - -class CodeInfo(object): - - id_regex = re.compile(r'[^\d\W][\w\.]*', re.UNICODE) - func_call_regex = re.compile(r'([^\d\W][\w\.]*)\([^\)\()]*\Z', - re.UNICODE) - - def __init__(self, name, source_code, position, filename=None, - is_python_like=True, **kwargs): - self.__dict__.update(kwargs) - self.name = name - self.filename = filename - self.source_code = source_code - self.position = position - self.is_python_like = is_python_like - - if position == 0: - self.lines = [] - self.line_num = 0 - self.obj = None - self.full_obj = None - else: - self._get_info() - - def _get_info(self): - self.lines = self.source_code[:self.position].splitlines() - self.line_num = len(self.lines) - - self.line = self.lines[-1] - self.column = len(self.lines[-1]) - - tokens = re.findall(self.id_regex, self.line) - if tokens and self.line.endswith(tokens[-1]): - self.obj = tokens[-1] - else: - self.obj = None - - self.full_obj = self.obj - - if self.obj: - full_line = self.source_code.splitlines()[self.line_num - 1] - rest = full_line[self.column:] - match = re.match(self.id_regex, rest) - if match: - self.full_obj = self.obj + match.group() - - if (self.name in ['info', 'definition'] and (not self.obj) - and self.is_python_like): - func_call = re.findall(self.func_call_regex, self.line) - if func_call: - self.obj = func_call[-1] - self.column = self.line.index(self.obj) + len(self.obj) - self.position = self.position - len(self.line) + self.column - - def split_words(self, position=None): - """ - Split our source code into valid identifiers. - - P""" - if position is None: - position = self.offset - text = self.source_code[:position] - return re.findall(self.id_regex, text) - - def __eq__(self, other): - try: - return self.__dict__ == other.__dict__ - except Exception: - return False - - -class PluginManager(QObject): - - send_to_inspector = Signal(str, str, str, str, bool) - edit_goto = Signal(str, int, str) - - def __init__(self, editor_widget): - super(PluginManager, self).__init__() - self.editor_widget = editor_widget - self.pending = None - self.busy = False - self.load_plugins() - self._submods_thread = GetSubmodulesThread() - self._submods_thread.finished.connect(self._update_extension_modules) - self._submods_thread.start() - - def load_plugins(self): - """Get and load a plugin, checking in order of PLUGINS""" - plugins = OrderedDict() - for plugin_name in PLUGINS: - mod_name = plugin_name + '_plugin' - try: - mod = __import__('spyderlib.utils.introspection.' + mod_name, - fromlist=[mod_name]) - cls = getattr(mod, '%sPlugin' % plugin_name.capitalize()) - plugin = cls() - plugin.load_plugin() - except Exception as e: - debug_print(e) - if DEBUG_EDITOR: - log_last_error(LOG_FILENAME) - else: - plugins[plugin_name] = plugin - debug_print('Instropection Plugin Loaded: %s' % plugin.name) - self.plugins = plugins - debug_print('Plugins loaded: %s' % self.plugins.keys()) - return plugins - - def _get_code_info(self, name, position=None, **kwargs): - - editor = self.editor_widget.get_current_editor() - finfo = self.editor_widget.get_current_finfo() - - if position is None: - position = editor.get_position('cursor') - - kwargs['editor'] = editor - kwargs['finfo'] = finfo - kwargs['editor_widget'] = self.editor_widget - - return CodeInfo(name, finfo.get_source_code(), position, - finfo.filename, editor.is_python_like, **kwargs) - - def get_completions(self, automatic): - """Get code completion""" - info = self._get_code_info('completions', automatic=automatic) - - if 'jedi' in self.plugins and not self.plugins['jedi'].busy: - self._handle_request(info) - - elif info.line.lstrip().startswith(('import ', 'from ')): - self._handle_request(info, 'fallback') - - else: - self._handle_request(info) - - def go_to_definition(self, position): - """Go to definition""" - info = self._get_code_info('definition', position) - - self._handle_request(info) - - def show_object_info(self, position, auto=True): - """Show signature calltip and/or docstring in the Object Inspector""" - # auto is True means that this method was called automatically, - # i.e. the user has just entered an opening parenthesis -- in that - # case, we don't want to force the object inspector to be visible, - # to avoid polluting the window layout - info = self._get_code_info('info', position, auto=auto) - self._handle_request(info) - - def validate(self): - """Validate the plugins""" - if not self.busy: - for plugin in self.plugins.values(): - plugin.validate() - - def is_editor_ready(self): - """Check if the main app is starting up""" - if self.editor_widget: - window = self.editor_widget.window() - if hasattr(window, 'is_starting_up') and not window.is_starting_up: - return True - - def _handle_request(self, info, desired=None): - """Handle an incoming request from the user.""" - debug_print('%s request:' % info.name) - - editor = info.editor - if ((not editor.is_python_like()) - or sourcecode.is_keyword(info.obj) - or editor.in_comment_or_string()): - desired = 'fallback' - - self.pending = (info, desired) - if not self.busy: - self._handle_pending() - - def _handle_pending(self): - """Handle any pending requests, sending them to the correct plugin.""" - if not self.pending: - self._post_message('') - return - info, desired = self.pending - if desired and self.plugins[desired].busy: - return - self.busy = True - - if desired: - plugins = [self.plugins[desired]] - elif info.name == 'definition' and not info.editor.is_python(): - plugins = [p for p in self.plugins.values() if not p.busy] - else: - # use all but the fallback - plugins = [p for p in list(self.plugins.values())[:-1] if not p.busy] - - self.request = RequestHandler(info, plugins) - self.request.introspection_complete.connect( - self._introspection_complete) - self.pending = None - - def _introspection_complete(self): - """ - Handle an introspection response from the thread. - - Route the response to the correct handler, and then handle - any pending requests. - """ - self.busy = False - result = self.request.result - info = self.request.info - current = self._get_code_info('current') - - if result and current.filename == info.filename: - func = getattr(self, '_handle_%s_response' % info.name) - try: - func(result, current, info) - except Exception as e: - debug_print(e) - elif current.filename == info.filename and info.name == 'definition': - result = self.plugins['fallback'].get_definition(info) - - if info == self.pending: - self.pending = None - - self._handle_pending() - - def _handle_completions_response(self, comp_list, info, prev_info): - """ - Handle a `completions` response. - - Only handle the response if we are on the same line of text and - on the same `obj` as the original request. - """ - if info.line_num != prev_info.line_num: - return - completion_text = info.obj - prev_text = prev_info.obj - - if prev_info.obj is None: - completion_text = '' - prev_text = '' - - if not completion_text.startswith(prev_text): - return - - if info.full_obj and len(info.full_obj) > len(info.obj): - new_list = [c for c in comp_list if c.startswith(info.full_obj)] - if new_list: - pos = info.editor.get_position('cursor') - new_pos = pos + len(info.full_obj) - len(info.obj) - info.editor.set_cursor_position(new_pos) - completion_text = info.full_obj - comp_list = new_list - - if '.' in completion_text: - completion_text = completion_text.split('.')[-1] - - comp_list = [c.split('.')[-1] for c in comp_list] - comp_list = [c for c in comp_list if c.startswith(completion_text)] - - info.editor.show_completion_list(comp_list, completion_text, - prev_info.automatic) - - def _handle_info_response(self, resp, info, prev_info): - """ - Handle an `info` response, triggering a calltip and/or docstring. - - Only handle the response if we are on the same line of text as - when the request was initiated. - """ - if info.line_num != prev_info.line_num: - return - - if resp['calltip']: - info.editor.show_calltip('Arguments', resp['calltip'], - signature=True, - at_position=prev_info.position) - - if resp['name']: - self.send_to_inspector.emit( - resp['name'], resp['argspec'], - resp['note'], resp['docstring'], - not prev_info.auto) - - def _handle_definition_response(self, resp, info, prev_info): - """Handle a `definition` response""" - fname, lineno = resp - self.edit_goto.emit(fname, lineno, "") - - def _update_extension_modules(self): - """Set the extension_modules after submods thread finishes""" - for plugin in self.plugins.values(): - plugin.set_pref('extension_modules', - self._submods_thread.submods) - - def _post_message(self, message, timeout=60000): - """ - Post a message to the main window status bar with a timeout in ms - """ - if self.editor_widget: - statusbar = self.editor_widget.window().statusBar() - statusbar.showMessage(message, timeout) - QApplication.processEvents() - - -def memoize(obj): - """ - Memoize objects to trade memory for execution speed - - Use a limited size cache to store the value, which takes into account - The calling args and kwargs - - See https://wiki.python.org/moin/PythonDecoratorLibrary#Memoize - """ - cache = obj.cache = {} - - @functools.wraps(obj) - def memoizer(*args, **kwargs): - key = str(args) + str(kwargs) - if key not in cache: - cache[key] = obj(*args, **kwargs) - # only keep the most recent 100 entries - if len(cache) > 100: - cache.popitem(last=False) - return cache[key] - return memoizer - - -class IntrospectionPlugin(object): - - busy = False - - def load_plugin(self): - """Initialize the plugin""" - pass - - def get_completions(self, info): - """Get a list of completions""" - pass - - def get_info(self, info): - """ - Find the calltip and docs - - Returns a dict like the following: - {'note': 'Function of numpy.core.numeric...', - 'argspec': "(shape, dtype=None, order='C')' - 'docstring': 'Return an array of given...' - 'name': 'ones', - 'calltip': 'ones(shape, dtype=None, order='C')'} - """ - pass - - def get_definition(self, info): - """Get a (filename, line_num) location for a definition""" - pass - - def set_pref(self, name, value): - """Set a plugin preference to a value""" - pass - - def validate(self): - """Validate the plugin""" - pass - - @staticmethod - @memoize - def get_parent_until(path): - """ - Given a file path, determine the full module path - - e.g. '/usr/lib/python2.7/dist-packages/numpy/core/__init__.pyc' yields - 'numpy.core' - """ - dirname = osp.dirname(path) - try: - mod = osp.basename(path) - mod = osp.splitext(mod)[0] - imp.find_module(mod, [dirname]) - except ImportError: - return - items = [mod] - while 1: - items.append(osp.basename(dirname)) - try: - dirname = osp.dirname(dirname) - imp.find_module('__init__', [dirname + os.sep]) - except ImportError: - break - return '.'.join(reversed(items)) - - -if __name__ == '__main__': - code = 'import numpy' - test = CodeInfo('test', code, len(code) - 2) - assert test.obj == 'num' - assert test.full_obj == 'numpy' - test2 = CodeInfo('test', code, len(code) - 2) - assert test == test2 diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/utils/introspection/plugin_server.py spyder-3.0.2+dfsg1/spyder/utils/introspection/plugin_server.py --- spyder-2.3.8+dfsg1/spyder/utils/introspection/plugin_server.py 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/utils/introspection/plugin_server.py 2016-10-25 02:05:23.000000000 +0200 @@ -0,0 +1,129 @@ +# -*- coding: utf-8 -*- +# +# Copyright © Spyder Project Contributors +# Licensed under the terms of the MIT License +# (see spyder/__init__.py for details) + +import sys +import time +import traceback + +import zmq + + +# Timeout in milliseconds +TIMEOUT = 10000 + + +class AsyncServer(object): + + """ + Introspection server, provides a separate process + for interacting with an object. + """ + + def __init__(self, port, *args): + self.port = port + self.object = self.initialize(*args) + self.context = zmq.Context() + self.socket = self.context.socket(zmq.PAIR) + self.socket.connect("tcp://localhost:%s" % port) + self.socket.send_pyobj(port) + + def initialize(self, plugin_name): + """Initialize the object and return it. + """ + return object() + + def run(self): + """Handle requests from the client. + """ + t0 = time.time() + initialized = False + timed_out = False + while 1: + # Poll for events, handling a timeout. + try: + events = self.socket.poll(TIMEOUT) + except KeyboardInterrupt: + time.sleep(0.1) + continue + if events == 0 and initialized: + if timed_out: + delta = int(time.time() - t0) + print('Timed out after %s sec' % delta) + return + timed_out = True + continue + timed_out = False + initialized = True + # Drain all exising requests, handling quit and heartbeat. + requests = [] + while 1: + try: + request = self.socket.recv_pyobj() + except KeyboardInterrupt: + time.sleep(0.1) + continue + if request['func_name'] == 'server_quit': + print('Quitting') + sys.stdout.flush() + return + elif request['func_name'] != 'server_heartbeat': + requests.append(request) + else: + print('Got heartbeat') + try: + events = self.socket.poll(0) + except KeyboardInterrupt: + time.sleep(0.1) + continue + if events == 0: + break + # Select the most recent request. + if not requests: + continue + request = requests[-1] + + # Gather the response + response = dict(func_name=request['func_name'], + request_id=request['request_id']) + try: + func = getattr(self.object, request['func_name']) + args = request.get('args', []) + kwargs = request.get('kwargs', {}) + response['result'] = func(*args, **kwargs) + except Exception: + response['error'] = traceback.format_exc() + + # Send the response to the client. + self.socket.send_pyobj(response) + + +class PluginServer(AsyncServer): + + """ + Introspection plugin server, provides a separate process + for interacting with a plugin. + """ + + def initialize(self, plugin_name): + """Initialize the object and return it. + """ + mod_name = plugin_name + '_plugin' + mod = __import__('spyder.utils.introspection.' + mod_name, + fromlist=[mod_name]) + cls = getattr(mod, '%sPlugin' % plugin_name.capitalize()) + plugin = cls() + plugin.load_plugin() + return plugin + + +if __name__ == '__main__': + args = sys.argv[1:] + if not len(args) == 2: + print('Usage: plugin_server.py client_port plugin_name') + sys.exit(0) + plugin = PluginServer(*args) + print('Started') + plugin.run() diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/utils/introspection/rope_plugin.py spyder-3.0.2+dfsg1/spyder/utils/introspection/rope_plugin.py --- spyder-2.3.8+dfsg1/spyder/utils/introspection/rope_plugin.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/utils/introspection/rope_plugin.py 2016-10-25 02:05:23.000000000 +0200 @@ -1,30 +1,34 @@ # -*- coding: utf-8 -*- # -# Copyright © 2013 The Spyder Development Team +# Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) +# (see spyder/__init__.py for details) """ Rope introspection plugin """ import time +import imp -from spyderlib import dependencies -from spyderlib.baseconfig import get_conf_path, _, STDERR -from spyderlib.utils import encoding, programs -from spyderlib.py3compat import PY2 -from spyderlib.utils.dochelpers import getsignaturefromtext -from spyderlib.utils import sourcecode -from spyderlib.utils.debug import log_last_error, log_dt -from spyderlib.utils.introspection.plugin_manager import ( +from spyder.config.base import get_conf_path, STDERR +from spyder.utils import encoding, programs +from spyder.py3compat import PY2 +from spyder.utils.dochelpers import getsignaturefromtext +from spyder.utils import sourcecode +from spyder.utils.debug import log_last_error, log_dt +from spyder.utils.introspection.manager import ( DEBUG_EDITOR, LOG_FILENAME, IntrospectionPlugin) +from spyder.utils.introspection.module_completion import ( + get_preferred_submodules) +from spyder.utils.introspection.manager import ROPE_REQVER + try: try: - from spyderlib import rope_patch + from spyder import rope_patch rope_patch.apply() except ImportError: - # rope 0.9.2/0.9.3 is not installed + # rope is not installed pass import rope.base.libutils import rope.contrib.codeassist @@ -32,11 +36,6 @@ pass -ROPE_REQVER = '>=0.9.2' -dependencies.add('rope', - _("Editor's code completion, go-to-definition and help"), - required_version=ROPE_REQVER) - #TODO: The following preferences should be customizable in the future ROPE_PREFS = {'ignore_syntax_errors': True, 'ignore_bad_imports': True, @@ -48,29 +47,47 @@ class RopePlugin(IntrospectionPlugin): """ Rope based introspection plugin for jedi - + Editor's code completion, go-to-definition and help """ - + project = None - + # ---- IntrospectionPlugin API -------------------------------------------- name = 'rope' - + def load_plugin(self): """Load the Rope introspection plugin""" if not programs.is_module_installed('rope', ROPE_REQVER): raise ImportError('Requires Rope %s' % ROPE_REQVER) self.project = None self.create_rope_project(root_path=get_conf_path()) + submods = get_preferred_submodules() + actual = [] + for submod in submods: + try: + imp.find_module(submod) + actual.append(submod) + except ImportError: + pass + if self.project is not None: + self.project.prefs.set('extension_modules', actual) def get_completions(self, info): - """Get a list of completions using Rope""" + """Get a list of (completion, type) tuples using Rope""" if self.project is None: - return - filename = info.filename - source_code = info.source_code - offset = info.position + return [] + filename = info['filename'] + source_code = info['source_code'] + offset = info['position'] + + # Prevent Rope from returning import completions because + # it can't handle them. Only Jedi can do it! + lines = sourcecode.split_source(source_code[:offset]) + last_line = lines[-1].lstrip() + if (last_line.startswith('import ') or last_line.startswith('from ')) \ + and not ';' in last_line: + return [] if PY2: filename = filename.encode('utf-8') @@ -93,18 +110,19 @@ proposals = rope.contrib.codeassist.sorted_proposals(proposals) if DEBUG_EDITOR: log_dt(LOG_FILENAME, "code_assist/sorted_proposals", t0) - return [proposal.name for proposal in proposals] + return [(proposal.name, proposal.type) for proposal in proposals] except Exception as _error: #analysis:ignore if DEBUG_EDITOR: log_last_error(LOG_FILENAME, "get_completion_list") + return [] def get_info(self, info): """Get a formatted calltip and docstring from Rope""" if self.project is None: return - filename = info.filename - source_code = info.source_code - offset = info.position + filename = info['filename'] + source_code = info['source_code'] + offset = info['position'] if PY2: filename = filename.encode('utf-8') @@ -182,6 +200,9 @@ module = obj_fullname[:module_end] note = 'Present in %s module' % module + if not doc_text and not calltip: + return + return dict(name=obj_fullname, argspec=argspec, note=note, docstring=doc_text, calltip=calltip) @@ -190,9 +211,9 @@ if self.project is None: return - filename = info.filename - source_code = info.source_code - offset = info.position + filename = info['filename'] + source_code = info['source_code'] + offset = info['position'] if PY2: filename = filename.encode('utf-8') @@ -225,12 +246,10 @@ def validate(self): """Validate the Rope project""" if self.project is not None: - self.project.validate(self.project.root) - - def set_pref(self, key, value): - """Set a Rope preference""" - if self.project is not None: - self.project.prefs.set(key, value) + try: + self.project.validate(self.project.root) + except RuntimeError: + pass # ---- Private API ------------------------------------------------------- @@ -252,8 +271,6 @@ log_last_error(LOG_FILENAME, "create_rope_project: %r" % root_path) except TypeError: - # Compatibility with new Mercurial API (>= 1.3). - # New versions of rope (> 0.9.2) already handle this issue self.project = None if DEBUG_EDITOR: log_last_error(LOG_FILENAME, @@ -268,8 +285,8 @@ if __name__ == '__main__': - from spyderlib.utils.introspection.plugin_manager import CodeInfo - + from spyder.utils.introspection.manager import CodeInfo + p = RopePlugin() p.load_plugin() @@ -277,16 +294,16 @@ docs = p.get_info(CodeInfo('info', source_code, len(source_code), __file__)) assert 'ones(' in docs['calltip'] and 'ones(' in docs['docstring'] - + source_code = "import numpy; n" completions = p.get_completions(CodeInfo('completions', source_code, len(source_code), __file__)) - assert 'numpy' in completions - - source_code = "import matplotlib.pyplot as plt; plt.imsave" - path, line_nr = p.get_definition(CodeInfo('definition', source_code, + assert ('numpy', 'module') in completions + + source_code = "import a" + completions = p.get_completions(CodeInfo('completions', source_code, len(source_code), __file__)) - assert 'pyplot.py' in path + assert not completions code = ''' def test(a, b): @@ -294,8 +311,9 @@ pass test(1,''' path, line = p.get_definition(CodeInfo('definition', code, len(code), - 'dummy.txt')) + 'dummy.txt', is_python_like=True)) assert line == 2 - docs = p.get_info(CodeInfo('info', code, len(code), __file__)) + docs = p.get_info(CodeInfo('info', code, len(code), __file__, + is_python_like=True)) assert 'Test docstring' in docs['docstring'] diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/utils/introspection/utils.py spyder-3.0.2+dfsg1/spyder/utils/introspection/utils.py --- spyder-2.3.8+dfsg1/spyder/utils/introspection/utils.py 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/utils/introspection/utils.py 2016-10-25 02:05:23.000000000 +0200 @@ -0,0 +1,248 @@ +# -*- coding: utf-8 -*- +# +# Copyright © Spyder Project Contributors +# Licensed under the terms of the MIT License +# (see spyder/__init__.py for details) + +""" +Introspection utilities used by Spyder +""" + +import imp +import os +import pickle +import os.path as osp +import re + +from spyder.utils.misc import memoize + +from spyder.utils.syntaxhighlighters import ( + custom_extension_lexer_mapping +) + +from pygments.lexer import words +from pygments.lexers import ( + get_lexer_for_filename, get_lexer_by_name, TextLexer +) +from pygments.util import ClassNotFound +from pygments.token import Token + + +class CodeInfo(object): + + id_regex = re.compile(r'[^\d\W][\w\.]*', re.UNICODE) + func_call_regex = re.compile(r'([^\d\W][\w\.]*)\([^\)\()]*\Z', + re.UNICODE) + + def __init__(self, name, source_code, position, filename=None, + is_python_like=False, in_comment_or_string=False, **kwargs): + self.__dict__.update(kwargs) + self.name = name + self.filename = filename + self.source_code = source_code + self.is_python_like = is_python_like + self.in_comment_or_string = in_comment_or_string + + self.position = position + + # if in a comment, look for the previous definition + if in_comment_or_string: + # if this is a docstring, find it, set as our + self.docstring = self._get_docstring() + # backtrack and look for a line that starts with def or class + if name != 'completions': + while position: + base = self.source_code[position: position + 6] + if base.startswith('def ') or base.startswith('class '): + position += base.index(' ') + 1 + break + position -= 1 + else: + self.docstring = '' + + self.position = position + + if position == 0: + self.lines = [] + self.column = 0 + self.line_num = 0 + self.line = '' + self.obj = '' + self.full_obj = '' + else: + self._get_info() + + def _get_info(self): + + self.lines = self.source_code[:self.position].splitlines() + self.line_num = len(self.lines) + + self.line = self.lines[-1] + self.column = len(self.lines[-1]) + + full_line = self.source_code.splitlines()[self.line_num - 1] + + lexer = find_lexer_for_filename(self.filename) + + # check for a text-based lexer that doesn't split tokens + if len(list(lexer.get_tokens('a b'))) == 1: + # Use regex to get the information + tokens = re.findall(self.id_regex, self.line) + if tokens and self.line.endswith(tokens[-1]): + self.obj = tokens[-1] + else: + self.obj = None + + self.full_obj = self.obj + + if self.obj: + full_line = self.source_code.splitlines()[self.line_num - 1] + rest = full_line[self.column:] + match = re.match(self.id_regex, rest) + if match: + self.full_obj = self.obj + match.group() + + self.context = None + else: + # Use lexer to get the information + pos = 0 + line_tokens = lexer.get_tokens(full_line) + for (context, token) in line_tokens: + pos += len(token) + if pos >= self.column: + self.obj = token[:len(token) - (pos - self.column)] + self.full_obj = token + if context in Token.Literal.String: + context = Token.Literal.String + self.context = context + break + + if (self.name in ['info', 'definition'] and (not self.context in Token.Name) + and self.is_python_like): + func_call = re.findall(self.func_call_regex, self.line) + if func_call: + self.obj = func_call[-1] + self.column = self.line.index(self.obj) + len(self.obj) + self.position = self.position - len(self.line) + self.column + + def _get_docstring(self): + """Find the docstring we are currently in""" + left = self.position + while left: + if self.source_code[left: left + 3] in ['"""', "'''"]: + left += 3 + break + left -= 1 + right = self.position + while right < len(self.source_code): + if self.source_code[right - 3: right] in ['"""', "'''"]: + right -= 3 + break + right += 1 + if left and right < len(self.source_code): + return self.source_code[left: right] + return '' + + def __eq__(self, other): + try: + return self.serialize() == other.serialize() + except Exception: + return False + + def __getitem__(self, item): + """Allow dictionary-like access""" + return getattr(self, item) + + def serialize(self): + state = {} + for (key, value) in self.__dict__.items(): + try: + pickle.dumps(value) + state[key] = value + except Exception: + pass + state['id_regex'] = self.id_regex + state['func_call_regex'] = self.func_call_regex + return state + + +def find_lexer_for_filename(filename): + """Get a Pygments Lexer given a filename. + """ + filename = filename or '' + root, ext = os.path.splitext(filename) + if ext in custom_extension_lexer_mapping: + lexer = get_lexer_by_name(custom_extension_lexer_mapping[ext]) + else: + try: + lexer = get_lexer_for_filename(filename) + except ClassNotFound: + return TextLexer() + return lexer + + +def get_keywords(lexer): + """Get the keywords for a given lexer. + """ + if not hasattr(lexer, 'tokens'): + return [] + if 'keywords' in lexer.tokens: + try: + return lexer.tokens['keywords'][0][0].words + except: + pass + keywords = [] + for vals in lexer.tokens.values(): + for val in vals: + try: + if isinstance(val[0], words): + keywords.extend(val[0].words) + else: + ini_val = val[0] + if ')\\b' in val[0] or ')(\\s+)' in val[0]: + val = re.sub(r'\\.', '', val[0]) + val = re.sub('[^0-9a-zA-Z|]+', '', val) + if '|' in ini_val: + keywords.extend(val.split('|')) + else: + keywords.append(val) + except Exception: + continue + return keywords + + +@memoize +def get_parent_until(path): + """ + Given a file path, determine the full module path + + e.g. '/usr/lib/python2.7/dist-packages/numpy/core/__init__.pyc' yields + 'numpy.core' + """ + dirname = osp.dirname(path) + try: + mod = osp.basename(path) + mod = osp.splitext(mod)[0] + imp.find_module(mod, [dirname]) + except ImportError: + return + items = [mod] + while 1: + items.append(osp.basename(dirname)) + try: + dirname = osp.dirname(dirname) + imp.find_module('__init__', [dirname + os.sep]) + except ImportError: + break + return '.'.join(reversed(items)) + + +if __name__ == '__main__': + code = 'import numpy' + test = CodeInfo('test', code, len(code) - 2) + assert test.obj == 'num' + assert test.full_obj == 'numpy' + test2 = CodeInfo('test', code, len(code) - 2) + assert test == test2 + test3 = pickle.loads(pickle.dumps(test2.__dict__)) + assert test3['full_obj'] == 'numpy' diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/utils/iofuncs.py spyder-3.0.2+dfsg1/spyder/utils/iofuncs.py --- spyder-2.3.8+dfsg1/spyder/utils/iofuncs.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/utils/iofuncs.py 2016-10-25 02:05:23.000000000 +0200 @@ -1,8 +1,8 @@ # -*- coding:utf-8 -*- # -# Copyright © 2009-2010 Pierre Raybaut +# Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) +# (see spyder/__init__.py for details) """ Input/Output Utilities @@ -33,7 +33,8 @@ pd = None #analysis:ignore # Local imports -from spyderlib.py3compat import pickle, to_text_string, getcwd, PY2 +from spyder.config.base import _, get_conf_path +from spyder.py3compat import pickle, to_text_string, getcwd, PY2 class MatlabStruct(dict): @@ -45,7 +46,7 @@ Examples ======== - >>> from spyderlib.utils.iofuncs import MatlabStruct + >>> from spyder.utils.iofuncs import MatlabStruct >>> a = MatlabStruct() >>> a.b = 'spam' # a["b"] == 'spam' >>> a.c["d"] = 'eggs' # a.c.d == 'eggs' @@ -214,7 +215,7 @@ try: - from spyderlib.pil_patch import Image + from spyder.pil_patch import Image if sys.byteorder == 'little': _ENDIAN = '<' @@ -271,7 +272,11 @@ def load_json(filename): """Load a json file as a dictionary""" try: - with open(filename, 'rb') as fid: + if PY2: + args = 'rb' + else: + args = 'r' + with open(filename, args) as fid: data = json.load(fid) return data, None except Exception as err: @@ -371,99 +376,6 @@ return data, error_message -from spyderlib.baseconfig import get_conf_path, STDERR - -SAVED_CONFIG_FILES = ('inspector', 'onlinehelp', 'path', 'pylint.results', - 'spyder.ini', 'temp.py', 'temp.spydata', 'template.py', - 'history.py', 'history_internal.py', 'workingdir', - '.projects', '.spyderproject', '.ropeproject', - 'monitor.log', 'monitor_debug.log', 'rope.log') - -def reset_session(): - """Remove all config files""" - print("*** Reset Spyder settings to defaults ***", file=STDERR) - for fname in SAVED_CONFIG_FILES: - cfg_fname = get_conf_path(fname) - if osp.isfile(cfg_fname): - os.remove(cfg_fname) - elif osp.isdir(cfg_fname): - shutil.rmtree(cfg_fname) - else: - continue - print("removing:", cfg_fname, file=STDERR) - - -def save_session(filename): - """Save Spyder session""" - local_fname = get_conf_path(osp.basename(filename)) - filename = osp.abspath(filename) - old_cwd = getcwd() - os.chdir(get_conf_path()) - error_message = None - try: - tar = tarfile.open(local_fname, "w") - for fname in SAVED_CONFIG_FILES: - if osp.isfile(fname): - tar.add(fname) - tar.close() - shutil.move(local_fname, filename) - except Exception as error: - error_message = to_text_string(error) - os.chdir(old_cwd) - return error_message - - -def load_session(filename): - """Load Spyder session""" - filename = osp.abspath(filename) - old_cwd = getcwd() - os.chdir(osp.dirname(filename)) - error_message = None - renamed = False - try: - tar = tarfile.open(filename, "r") - extracted_files = tar.getnames() - - # Rename original config files - for fname in extracted_files: - orig_name = get_conf_path(fname) - bak_name = get_conf_path(fname+'.bak') - if osp.isfile(bak_name): - os.remove(bak_name) - if osp.isfile(orig_name): - os.rename(orig_name, bak_name) - renamed = True - - tar.extractall() - - for fname in extracted_files: - shutil.move(fname, get_conf_path(fname)) - - except Exception as error: - error_message = to_text_string(error) - if renamed: - # Restore original config files - for fname in extracted_files: - orig_name = get_conf_path(fname) - bak_name = get_conf_path(fname+'.bak') - if osp.isfile(orig_name): - os.remove(orig_name) - if osp.isfile(bak_name): - os.rename(bak_name, orig_name) - - finally: - # Removing backup config files - for fname in extracted_files: - bak_name = get_conf_path(fname+'.bak') - if osp.isfile(bak_name): - os.remove(bak_name) - - os.chdir(old_cwd) - return error_message - - -from spyderlib.baseconfig import _ - class IOFunctions(object): def __init__(self): self.load_extensions = None @@ -523,8 +435,8 @@ def get_3rd_party_funcs(self): other_funcs = [] - from spyderlib.otherplugins import get_spyderplugins_mods - for mod in get_spyderplugins_mods(prefix='io_', extension='.py'): + from spyder.otherplugins import get_spyderplugins_mods + for mod in get_spyderplugins_mods(io=True): try: other_funcs.append((mod.FORMAT_EXT, mod.FORMAT_NAME, mod.FORMAT_LOAD, mod.FORMAT_SAVE)) @@ -556,7 +468,7 @@ if __name__ == "__main__": - from spyderlib.py3compat import u + from spyder.py3compat import u import datetime testdict = {'d': 1, 'a': np.random.rand(10, 10), 'b': [1, 2]} testdate = datetime.date(1945, 5, 8) diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/utils/ipython/__init__.py spyder-3.0.2+dfsg1/spyder/utils/ipython/__init__.py --- spyder-2.3.8+dfsg1/spyder/utils/ipython/__init__.py 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/utils/ipython/__init__.py 2016-10-25 02:05:23.000000000 +0200 @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +# +# Copyright © Spyder Project Contributors +# Licensed under the terms of the MIT License +# (see spyder/__init__.py for details) + +""" +Utilities for the IPython console +""" diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/utils/ipython/spyder_kernel.py spyder-3.0.2+dfsg1/spyder/utils/ipython/spyder_kernel.py --- spyder-2.3.8+dfsg1/spyder/utils/ipython/spyder_kernel.py 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/utils/ipython/spyder_kernel.py 2016-11-16 17:12:04.000000000 +0100 @@ -0,0 +1,340 @@ +# -*- coding: utf-8 -*- +# +# Copyright © Spyder Project Contributors +# Licensed under the terms of the MIT License +# (see spyder/__init__.py for details) + +""" +Spyder kernel for Jupyter +""" + +# Standard library imports +import os + +# Third-party imports +from ipykernel.datapub import publish_data +from ipykernel.ipkernel import IPythonKernel +import ipykernel.pickleutil +from ipykernel.pickleutil import CannedObject +from ipykernel.serialize import deserialize_object + +# Check if we are running under an external interpreter +IS_EXT_INTERPRETER = os.environ.get('EXTERNAL_INTERPRETER', '').lower() == "true" + +# Local imports +if not IS_EXT_INTERPRETER: + from spyder.py3compat import is_text_string + from spyder.utils.dochelpers import isdefined, getdoc, getsource + from spyder.utils.iofuncs import iofunctions + from spyder.utils.misc import fix_reference_name + from spyder.widgets.variableexplorer.utils import (get_remote_data, + make_remote_view) +else: + # We add "spyder" to sys.path for external interpreters, so this works! + # See create_kernel_spec of plugins/ipythonconsole + from py3compat import is_text_string + from utils.dochelpers import isdefined, getdoc, getsource + from utils.iofuncs import iofunctions + from utils.misc import fix_reference_name + from widgets.variableexplorer.utils import (get_remote_data, + make_remote_view) + + +# XXX --- Disable canning for Numpy arrays for now --- +# This allows getting values between a Python 3 frontend +# and a Python 2 kernel, and viceversa, for several types of +# arrays. +# See this link for interesting ideas on how to solve this +# in the future: +# http://stackoverflow.com/q/30698004/438386 +ipykernel.pickleutil.can_map.pop('numpy.ndarray') + + +class SpyderKernel(IPythonKernel): + """Spyder kernel for Jupyter""" + + def __init__(self, *args, **kwargs): + super(SpyderKernel, self).__init__(*args, **kwargs) + + self.namespace_view_settings = {} + self._pdb_obj = None + self._pdb_step = None + + @property + def _pdb_frame(self): + """Return current Pdb frame if there is any""" + if self._pdb_obj is not None and self._pdb_obj.curframe is not None: + return self._pdb_obj.curframe + + @property + def _pdb_locals(self): + """ + Return current Pdb frame locals if available. Otherwise + return an empty dictionary + """ + if self._pdb_frame: + return self._pdb_obj.curframe_locals + else: + return {} + + # -- Public API --------------------------------------------------- + # For the Variable Explorer + def get_namespace_view(self): + """ + Return the namespace view + + This is a dictionary with the following structure + + {'a': {'color': '#800000', 'size': 1, 'type': 'str', 'view': '1'}} + + Here: + * 'a' is the variable name + * 'color' is the color used to show it + * 'size' and 'type' are self-evident + * and'view' is its value or the text shown in the last column + """ + settings = self.namespace_view_settings + if settings: + ns = self._get_current_namespace() + more_excluded_names = ['In', 'Out'] + view = make_remote_view(ns, settings, more_excluded_names) + return view + + def get_var_properties(self): + """ + Get some properties of the variables in the current + namespace + """ + settings = self.namespace_view_settings + if settings: + ns = self._get_current_namespace() + data = get_remote_data(ns, settings, mode='editable', + more_excluded_names=['In', 'Out']) + + properties = {} + for name, value in list(data.items()): + properties[name] = { + 'is_list': isinstance(value, (tuple, list)), + 'is_dict': isinstance(value, dict), + 'len': self._get_len(value), + 'is_array': self._is_array(value), + 'is_image': self._is_image(value), + 'is_data_frame': self._is_data_frame(value), + 'is_series': self._is_series(value), + 'array_shape': self._get_array_shape(value), + 'array_ndim': self._get_array_ndim(value) + } + + return properties + else: + return {} + + def get_value(self, name): + """Get the value of a variable""" + ns = self._get_current_namespace() + value = ns[name] + publish_data({'__spy_data__': value}) + + def set_value(self, name, value): + """Set the value of a variable""" + ns = self._get_reference_namespace(name) + value = deserialize_object(value)[0] + if isinstance(value, CannedObject): + value = value.get_object() + ns[name] = value + + def remove_value(self, name): + """Remove a variable""" + ns = self._get_reference_namespace(name) + ns.pop(name) + + def copy_value(self, orig_name, new_name): + """Copy a variable""" + ns = self._get_reference_namespace(orig_name) + ns[new_name] = ns[orig_name] + + def load_data(self, filename, ext): + """Load data from filename""" + glbs = self._mglobals() + + load_func = iofunctions.load_funcs[ext] + data, error_message = load_func(filename) + + if error_message: + return error_message + + for key in list(data.keys()): + new_key = fix_reference_name(key, blacklist=list(glbs.keys())) + if new_key != key: + data[new_key] = data.pop(key) + + try: + glbs.update(data) + except Exception as error: + return str(error) + + return None + + def save_namespace(self, filename): + """Save namespace into filename""" + ns = self._get_current_namespace() + settings = self.namespace_view_settings + data = get_remote_data(ns, settings, mode='picklable', + more_excluded_names=['In', 'Out']).copy() + return iofunctions.save(data, filename) + + # --- For Pdb + def get_pdb_step(self): + """Return info about pdb current frame""" + return self._pdb_step + + # --- For the Help plugin + def is_defined(self, obj, force_import=False): + """Return True if object is defined in current namespace""" + ns = self._get_current_namespace(with_magics=True) + return isdefined(obj, force_import=force_import, namespace=ns) + + def get_doc(self, objtxt): + """Get object documentation dictionary""" + obj, valid = self._eval(objtxt) + if valid: + return getdoc(obj) + + def get_source(self, objtxt): + """Get object source""" + obj, valid = self._eval(objtxt) + if valid: + return getsource(obj) + + def set_cwd(self, dirname): + """Set current working directory.""" + return os.chdir(dirname) + + # -- Private API --------------------------------------------------- + # --- For the Variable Explorer + def _get_current_namespace(self, with_magics=False): + """ + Return current namespace + + This is globals() if not debugging, or a dictionary containing + both locals() and globals() for current frame when debugging + """ + ns = {} + glbs = self._mglobals() + + if self._pdb_frame is None: + ns.update(glbs) + else: + ns.update(glbs) + ns.update(self._pdb_locals) + + # Add magics to ns so we can show help about them on the Help + # plugin + if with_magics: + line_magics = self.shell.magics_manager.magics['line'] + cell_magics = self.shell.magics_manager.magics['cell'] + ns.update(line_magics) + ns.update(cell_magics) + + return ns + + def _get_reference_namespace(self, name): + """ + Return namespace where reference name is defined + + It returns the globals() if reference has not yet been defined + """ + glbs = self._mglobals() + if self._pdb_frame is None: + return glbs + else: + lcls = self._pdb_locals + if name in lcls: + return lcls + else: + return glbs + + def _mglobals(self): + """Return current globals -- handles Pdb frames""" + if self._pdb_frame is not None: + return self._pdb_frame.f_globals + else: + return self.shell.user_ns + + def _get_len(self, var): + """Return sequence length""" + try: + return len(var) + except TypeError: + return None + + def _is_array(self, var): + """Return True if variable is a NumPy array""" + try: + import numpy + return isinstance(var, numpy.ndarray) + except ImportError: + return False + + def _is_image(self, var): + """Return True if variable is a PIL.Image image""" + try: + from PIL import Image + return isinstance(var, Image.Image) + except ImportError: + return False + + def _is_data_frame(self, var): + """Return True if variable is a DataFrame""" + try: + from pandas import DataFrame + return isinstance(var, DataFrame) + except: + return False + + def _is_series(self, var): + """Return True if variable is a Series""" + try: + from pandas import Series + return isinstance(var, Series) + except: + return False + + def _get_array_shape(self, var): + """Return array's shape""" + try: + if self._is_array(var): + return var.shape + else: + return None + except AttributeError: + return None + + def _get_array_ndim(self, var): + """Return array's ndim""" + try: + if self._is_array(var): + return var.ndim + else: + return None + except AttributeError: + return None + + # --- For Pdb + def _register_pdb_session(self, pdb_obj): + """Register Pdb session to use it later""" + self._pdb_obj = pdb_obj + + # --- For the Help plugin + def _eval(self, text): + """ + Evaluate text and return (obj, valid) + where *obj* is the object represented by *text* + and *valid* is True if object evaluation did not raise any exception + """ + assert is_text_string(text) + ns = self._get_current_namespace(with_magics=True) + try: + return eval(text, ns), True + except: + return None, False diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/utils/ipython/start_kernel.py spyder-3.0.2+dfsg1/spyder/utils/ipython/start_kernel.py --- spyder-2.3.8+dfsg1/spyder/utils/ipython/start_kernel.py 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/utils/ipython/start_kernel.py 2016-11-13 21:36:52.000000000 +0100 @@ -0,0 +1,223 @@ +# -*- coding: utf-8 -*- +# +# Copyright © Spyder Project Contributors +# Licensed under the terms of the MIT License +# (see spyder/__init__.py for details) + +""" +File used to start kernels for the IPython Console +""" + +# Standard library imports +import os +import os.path as osp +import sys + + +# Check if we are running under an external interpreter +IS_EXT_INTERPRETER = os.environ.get('EXTERNAL_INTERPRETER', '').lower() == "true" + + +def sympy_config(mpl_backend): + """Sympy configuration""" + if mpl_backend is not None: + lines = """ +from sympy.interactive import init_session +init_session() +%matplotlib {0} +""".format(mpl_backend) + else: + lines = """ +from sympy.interactive import init_session +init_session() +""" + + return lines + + +def kernel_config(): + """Create a config object with IPython kernel options""" + from IPython.core.application import get_ipython_dir + from traitlets.config.loader import Config, load_pyconfig_files + if not IS_EXT_INTERPRETER: + from spyder.config.main import CONF + from spyder.utils.programs import is_module_installed + else: + # We add "spyder" to sys.path for external interpreters, + # so this works! + # See create_kernel_spec of plugins/ipythonconsole + from config.main import CONF + from utils.programs import is_module_installed + + # ---- IPython config ---- + try: + profile_path = osp.join(get_ipython_dir(), 'profile_default') + cfg = load_pyconfig_files(['ipython_config.py', + 'ipython_kernel_config.py'], + profile_path) + except: + cfg = Config() + + # ---- Spyder config ---- + spy_cfg = Config() + + # Until we implement Issue 1052 + spy_cfg.InteractiveShell.xmode = 'Plain' + + # Run lines of code at startup + run_lines_o = CONF.get('ipython_console', 'startup/run_lines') + if run_lines_o: + spy_cfg.IPKernelApp.exec_lines = [x.strip() for x in run_lines_o.split(',')] + else: + spy_cfg.IPKernelApp.exec_lines = [] + + # Pylab configuration + mpl_backend = None + mpl_installed = is_module_installed('matplotlib') + pylab_o = CONF.get('ipython_console', 'pylab') + + if mpl_installed and pylab_o: + # Get matplotlib backend + backend_o = CONF.get('ipython_console', 'pylab/backend') + if backend_o == 1: + if is_module_installed('PyQt5'): + auto_backend = 'qt5' + elif is_module_installed('PyQt4'): + auto_backend = 'qt4' + elif is_module_installed('_tkinter'): + auto_backend = 'tk' + else: + auto_backend = 'inline' + else: + auto_backend = '' + backends = {0: 'inline', 1: auto_backend, 2: 'qt5', 3: 'qt4', + 4: 'osx', 5: 'gtk3', 6: 'gtk', 7: 'wx', 8: 'tk'} + mpl_backend = backends[backend_o] + + # Automatically load Pylab and Numpy, or only set Matplotlib + # backend + autoload_pylab_o = CONF.get('ipython_console', 'pylab/autoload') + if autoload_pylab_o: + spy_cfg.IPKernelApp.exec_lines.append( + "%pylab {0}".format(mpl_backend)) + else: + spy_cfg.IPKernelApp.exec_lines.append( + "%matplotlib {0}".format(mpl_backend)) + + # Inline backend configuration + if mpl_backend == 'inline': + # Figure format + format_o = CONF.get('ipython_console', + 'pylab/inline/figure_format', 0) + formats = {0: 'png', 1: 'svg'} + spy_cfg.InlineBackend.figure_format = formats[format_o] + + # Resolution + spy_cfg.InlineBackend.rc = {'figure.figsize': (6.0, 4.0), + 'savefig.dpi': 72, + 'font.size': 10, + 'figure.subplot.bottom': .125, + 'figure.facecolor': 'white', + 'figure.edgecolor': 'white' + } + resolution_o = CONF.get('ipython_console', + 'pylab/inline/resolution') + spy_cfg.InlineBackend.rc['savefig.dpi'] = resolution_o + + # Figure size + width_o = float(CONF.get('ipython_console', 'pylab/inline/width')) + height_o = float(CONF.get('ipython_console', 'pylab/inline/height')) + spy_cfg.InlineBackend.rc['figure.figsize'] = (width_o, height_o) + + # Run a file at startup + use_file_o = CONF.get('ipython_console', 'startup/use_run_file') + run_file_o = CONF.get('ipython_console', 'startup/run_file') + if use_file_o and run_file_o: + spy_cfg.IPKernelApp.file_to_run = run_file_o + + # Autocall + autocall_o = CONF.get('ipython_console', 'autocall') + spy_cfg.ZMQInteractiveShell.autocall = autocall_o + + # To handle the banner by ourselves in IPython 3+ + spy_cfg.ZMQInteractiveShell.banner1 = '' + + # Greedy completer + greedy_o = CONF.get('ipython_console', 'greedy_completer') + spy_cfg.IPCompleter.greedy = greedy_o + + # Sympy loading + sympy_o = CONF.get('ipython_console', 'symbolic_math') + if sympy_o and is_module_installed('sympy'): + lines = sympy_config(mpl_backend) + spy_cfg.IPKernelApp.exec_lines.append(lines) + + # Merge IPython and Spyder configs. Spyder prefs will have prevalence + # over IPython ones + cfg._merge(spy_cfg) + return cfg + + +def varexp(line): + """ + Spyder's variable explorer magic + + Used to generate plots, histograms and images of the variables displayed + on it. + """ + ip = get_ipython() #analysis:ignore + funcname, name = line.split() + import spyder.pyplot + __fig__ = spyder.pyplot.figure(); + __items__ = getattr(spyder.pyplot, funcname[2:])(ip.user_ns[name]) + spyder.pyplot.show() + del __fig__, __items__ + + +def main(): + # Remove this module's path from sys.path: + try: + sys.path.remove(osp.dirname(__file__)) + except ValueError: + pass + + try: + locals().pop('__file__') + except KeyError: + pass + __doc__ = '' + __name__ = '__main__' + + # Add current directory to sys.path (like for any standard Python interpreter + # executed in interactive mode): + sys.path.insert(0, '') + + # Fire up the kernel instance. + from ipykernel.kernelapp import IPKernelApp + + if not IS_EXT_INTERPRETER: + from spyder.utils.ipython.spyder_kernel import SpyderKernel + else: + # We add "spyder" to sys.path for external interpreters, + # so this works! + # See create_kernel_spec of plugins/ipythonconsole + from utils.ipython.spyder_kernel import SpyderKernel + + kernel = IPKernelApp.instance() + kernel.kernel_class = SpyderKernel + try: + kernel.config = kernel_config() + except: + pass + kernel.initialize() + + # NOTE: Leave this and other magic modifications *after* setting + # __ipythonkernel__ to not have problems while starting kernels + kernel.shell.register_magic_function(varexp) + + # Start the (infinite) kernel event loop. + kernel.start() + + +if __name__ == '__main__': + main() diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/utils/ipython/templates/blank.html spyder-3.0.2+dfsg1/spyder/utils/ipython/templates/blank.html --- spyder-2.3.8+dfsg1/spyder/utils/ipython/templates/blank.html 2015-08-24 21:09:46.000000000 +0200 +++ spyder-3.0.2+dfsg1/spyder/utils/ipython/templates/blank.html 2016-10-25 02:05:23.000000000 +0200 @@ -4,7 +4,7 @@ Blank page for the IPython Console - :copyright: Copyright 2014 by the Spyder Development Team + :copyright: Copyright by the Spyder Project Contributors :license: MIT license --> diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/utils/ipython/templates/kernel_error.html spyder-3.0.2+dfsg1/spyder/utils/ipython/templates/kernel_error.html --- spyder-2.3.8+dfsg1/spyder/utils/ipython/templates/kernel_error.html 2015-08-24 21:09:46.000000000 +0200 +++ spyder-3.0.2+dfsg1/spyder/utils/ipython/templates/kernel_error.html 2016-10-25 02:05:23.000000000 +0200 @@ -4,7 +4,7 @@ Kernel error template for the IPython Console - :copyright: Copyright 2014 by the Spyder Development Team + :copyright: Copyright by the Spyder Project Contributors :license: MIT license --> diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/utils/ipython/templates/loading.html spyder-3.0.2+dfsg1/spyder/utils/ipython/templates/loading.html --- spyder-2.3.8+dfsg1/spyder/utils/ipython/templates/loading.html 2015-08-24 21:09:46.000000000 +0200 +++ spyder-3.0.2+dfsg1/spyder/utils/ipython/templates/loading.html 2016-10-25 02:05:23.000000000 +0200 @@ -4,7 +4,7 @@ Loading template for the IPython console - :copyright: Copyright 2014 by the Spyder Development Team + :copyright: Copyright by the Spyder Project Contributors :license: MIT license --> diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/utils/misc.py spyder-3.0.2+dfsg1/spyder/utils/misc.py --- spyder-2.3.8+dfsg1/spyder/utils/misc.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/utils/misc.py 2016-10-25 02:05:23.000000000 +0200 @@ -1,16 +1,19 @@ # -*- coding: utf-8 -*- # -# Copyright © 2009-2010 Pierre Raybaut +# Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) +# (see spyder/__init__.py for details) """Miscellaneous utilities""" +import functools import os import os.path as osp import sys import stat +from spyder.py3compat import is_text_string + def __remove_pyc_pyo(fname): """Eventually remove .pyc and .pyo files associated to a Python script""" @@ -19,6 +22,7 @@ if osp.exists(fname+ending): os.remove(fname+ending) + def rename_file(source, dest): """ Rename file from *source* to *dest* @@ -27,6 +31,7 @@ os.rename(source, dest) __remove_pyc_pyo(source) + def remove_file(fname): """ Remove file *fname* @@ -35,6 +40,7 @@ os.remove(fname) __remove_pyc_pyo(fname) + def move_file(source, dest): """ Move file from *source* to *dest* @@ -47,11 +53,11 @@ def onerror(function, path, excinfo): """Error handler for `shutil.rmtree`. - - If the error is due to an access error (read-only file), it + + If the error is due to an access error (read-only file), it attempts to add write permission and then retries. If the error is for another reason, it re-raises the error. - + Usage: `shutil.rmtree(path, onerror=onerror)""" if not os.access(path, os.W_OK): # Is the error an access error? @@ -86,9 +92,9 @@ of *path* with names ending with *extensions* Directory names *excluded_dirnames* will be ignored""" if extensions is None: - extensions = ['.py', '.pyw', '.ipy', '.c', '.h', '.cpp', '.hpp', - '.inc', '.', '.hh', '.hxx', '.cc', '.cxx', '.cl', - '.f', '.for', '.f77', '.f90', '.f95', '.f2k'] + extensions = ['.py', '.pyw', '.ipy', '.enaml', '.c', '.h', '.cpp', + '.hpp', '.inc', '.', '.hh', '.hxx', '.cc', '.cxx', + '.cl', '.f', '.for', '.f77', '.f90', '.f95', '.f2k'] if excluded_dirnames is None: excluded_dirnames = ['build', 'dist', '.hg', '.svn'] def get_filelines(path): @@ -142,10 +148,10 @@ """Remove backslashes in *path* For Windows platforms only. - Returns the path unchanged on other platforms. - - This is especially useful when formatting path strings on - Windows platforms for which folder paths may contain backslashes + Returns the path unchanged on other platforms. + + This is especially useful when formatting path strings on + Windows platforms for which folder paths may contain backslashes and provoke unicode decoding errors in Python 3 (or in Python 2 when future 'unicode_literals' symbol has been imported).""" if os.name == 'nt': @@ -179,10 +185,10 @@ # (Tue Jan 15 19:13:25 CET 2008) """ Add the decorated method to the given class; replace as needed. - + If the named method already exists on the given class, it will - be replaced, and a reference to the old method is created as - cls._old. If the "_old__" attribute + be replaced, and a reference to the old method is created as + cls._old. If the "_old__" attribute already exists, KeyError is raised. """ def decorator(func): @@ -227,10 +233,69 @@ else: return osp.abspath(common) + +def add_pathlist_to_PYTHONPATH(env, pathlist, drop_env=False, + ipyconsole=False): + # PyQt API 1/2 compatibility-related tests: + assert isinstance(env, list) + assert all([is_text_string(path) for path in env]) + + pypath = "PYTHONPATH" + pathstr = os.pathsep.join(pathlist) + if os.environ.get(pypath) is not None and not drop_env: + old_pypath = os.environ[pypath] + if not ipyconsole: + for index, var in enumerate(env[:]): + if var.startswith(pypath+'='): + env[index] = var.replace(pypath+'=', + pypath+'='+pathstr+os.pathsep) + env.append('OLD_PYTHONPATH='+old_pypath) + else: + pypath = {'PYTHONPATH': pathstr + os.pathsep + old_pypath, + 'OLD_PYTHONPATH': old_pypath} + return pypath + else: + if not ipyconsole: + env.append(pypath+'='+pathstr) + else: + return {'PYTHONPATH': pathstr} + + +def memoize(obj): + """ + Memoize objects to trade memory for execution speed + + Use a limited size cache to store the value, which takes into account + The calling args and kwargs + + See https://wiki.python.org/moin/PythonDecoratorLibrary#Memoize + """ + cache = obj.cache = {} + + @functools.wraps(obj) + def memoizer(*args, **kwargs): + key = str(args) + str(kwargs) + if key not in cache: + cache[key] = obj(*args, **kwargs) + # only keep the most recent 100 entries + if len(cache) > 100: + cache.popitem(last=False) + return cache[key] + return memoizer + + if __name__ == '__main__': - assert get_common_path([ - 'D:\\Python\\spyder-v21\\spyderlib\\widgets', - 'D:\\Python\\spyder\\spyderlib\\utils', - 'D:\\Python\\spyder\\spyderlib\\widgets', - 'D:\\Python\\spyder-v21\\spyderlib\\utils', - ]) == 'D:\\Python' + if os.name == 'nt': + assert get_common_path([ + 'D:\\Python\\spyder-v21\\spyder\\widgets', + 'D:\\Python\\spyder\\spyder\\utils', + 'D:\\Python\\spyder\\spyder\\widgets', + 'D:\\Python\\spyder-v21\\spyder\\utils', + ]) == 'D:\\Python' + else: + assert get_common_path([ + '/Python/spyder-v21/spyder.widgets', + '/Python/spyder/spyder.utils', + '/Python/spyder/spyder.widgets', + '/Python/spyder-v21/spyder.utils', + ]) == '/Python' diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/utils/programs.py spyder-3.0.2+dfsg1/spyder/utils/programs.py --- spyder-2.3.8+dfsg1/spyder/utils/programs.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/utils/programs.py 2016-10-25 02:05:23.000000000 +0200 @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- # -# Copyright © 2009-2011 Pierre Raybaut +# Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) +# (see spyder/__init__.py for details) """Running programs utilities""" @@ -19,8 +19,12 @@ import tempfile # Local imports -from spyderlib.utils import encoding -from spyderlib.py3compat import PY2, is_text_string +from spyder.utils import encoding +from spyder.py3compat import PY2, is_text_string + + +class ProgramError(Exception): + pass if os.name == 'nt': @@ -31,8 +35,11 @@ def is_program_installed(basename): - """Return program absolute path if installed in PATH - Otherwise, return None""" + """ + Return program absolute path if installed in PATH. + + Otherwise, return None + """ for path in os.environ["PATH"].split(os.pathsep): abspath = osp.join(path, basename) if osp.isfile(abspath): @@ -40,9 +47,12 @@ def find_program(basename): - """Find program in PATH and return absolute path + """ + Find program in PATH and return absolute path + Try adding .exe or .bat to basename on Windows platforms - (return None if not found)""" + (return None if not found) + """ names = [basename] if os.name == 'nt': # Windows platforms @@ -55,21 +65,111 @@ return path -def run_program(name, args=[], cwd=None): - """Run program in a separate process""" - assert isinstance(args, (tuple, list)) - path = find_program(name) - if not path: - raise RuntimeError("Program %s was not found" % name) - subprocess.Popen([path]+args, cwd=cwd) +def alter_subprocess_kwargs_by_platform(**kwargs): + """ + Given a dict, populate kwargs to create a generally + useful default setup for running subprocess processes + on different platforms. For example, `close_fds` is + set on posix and creation of a new console window is + disabled on Windows. + + This function will alter the given kwargs and return + the modified dict. + """ + kwargs.setdefault('close_fds', os.name == 'posix') + if os.name == 'nt': + CONSOLE_CREATION_FLAGS = 0 # Default value + # See: https://msdn.microsoft.com/en-us/library/windows/desktop/ms684863%28v=vs.85%29.aspx + CREATE_NO_WINDOW = 0x08000000 + # We "or" them together + CONSOLE_CREATION_FLAGS |= CREATE_NO_WINDOW + kwargs.setdefault('creationflags', CONSOLE_CREATION_FLAGS) + return kwargs + + +def run_shell_command(cmdstr, **subprocess_kwargs): + """ + Execute the given shell command. + + Note that *args and **kwargs will be passed to the subprocess call. + + If 'shell' is given in subprocess_kwargs it must be True, + otherwise ProgramError will be raised. + . + If 'executable' is not given in subprocess_kwargs, it will + be set to the value of the SHELL environment variable. + + Note that stdin, stdout and stderr will be set by default + to PIPE unless specified in subprocess_kwargs. + + :str cmdstr: The string run as a shell command. + :subprocess_kwargs: These will be passed to subprocess.Popen. + """ + if 'shell' in subprocess_kwargs and not subprocess_kwargs['shell']: + raise ProgramError( + 'The "shell" kwarg may be omitted, but if ' + 'provided it must be True.') + else: + subprocess_kwargs['shell'] = True + + if 'executable' not in subprocess_kwargs: + subprocess_kwargs['executable'] = os.getenv('SHELL') + + for stream in ['stdin', 'stdout', 'stderr']: + subprocess_kwargs.setdefault(stream, subprocess.PIPE) + subprocess_kwargs = alter_subprocess_kwargs_by_platform( + **subprocess_kwargs) + return subprocess.Popen(cmdstr, **subprocess_kwargs) + + +def run_program(program, args=None, **subprocess_kwargs): + """ + Run program in a separate process. + + NOTE: returns the process object created by + `subprocess.Popen()`. This can be used with + `proc.communicate()` for example. + + If 'shell' appears in the kwargs, it must be False, + otherwise ProgramError will be raised. + + If only the program name is given and not the full path, + a lookup will be performed to find the program. If the + lookup fails, ProgramError will be raised. + + Note that stdin, stdout and stderr will be set by default + to PIPE unless specified in subprocess_kwargs. + + :str program: The name of the program to run. + :list args: The program arguments. + :subprocess_kwargs: These will be passed to subprocess.Popen. + """ + if 'shell' in subprocess_kwargs and subprocess_kwargs['shell']: + raise ProgramError( + "This function is only for non-shell programs, " + "use run_shell_command() instead.") + fullcmd = find_program(program) + if not fullcmd: + raise ProgramError("Program %s was not found" % program) + # As per subprocess, we make a complete list of prog+args + fullcmd = [fullcmd] + (args or []) + for stream in ['stdin', 'stdout', 'stderr']: + subprocess_kwargs.setdefault(stream, subprocess.PIPE) + subprocess_kwargs = alter_subprocess_kwargs_by_platform( + **subprocess_kwargs) + return subprocess.Popen(fullcmd, **subprocess_kwargs) def start_file(filename): - """Generalized os.startfile for all platforms supported by Qt - (this function is simply wrapping QDesktopServices.openUrl) - Returns True if successfull, otherwise returns False.""" - from spyderlib.qt.QtGui import QDesktopServices - from spyderlib.qt.QtCore import QUrl + """ + Generalized os.startfile for all platforms supported by Qt + + This function is simply wrapping QDesktopServices.openUrl + + Returns True if successfull, otherwise returns False. + """ + from qtpy.QtCore import QUrl + from qtpy.QtGui import QDesktopServices # We need to use setUrl instead of setPath because this is the only # cross-platform way to open external files. setPath fails completely on @@ -81,8 +181,10 @@ def python_script_exists(package=None, module=None): - """Return absolute path if Python script exists (otherwise, return None) - package=None -> module is in sys.path (standard library modules)""" + """ + Return absolute path if Python script exists (otherwise, return None) + package=None -> module is in sys.path (standard library modules) + """ assert module is not None try: if package is None: @@ -98,21 +200,25 @@ def run_python_script(package=None, module=None, args=[], p_args=[]): - """Run Python script in a separate process - package=None -> module is in sys.path (standard library modules)""" + """ + Run Python script in a separate process + package=None -> module is in sys.path (standard library modules) + """ assert module is not None assert isinstance(args, (tuple, list)) and isinstance(p_args, (tuple, list)) path = python_script_exists(package, module) - subprocess.Popen([sys.executable]+p_args+[path]+args) + run_program(sys.executable, p_args + [path] + args) def shell_split(text): - """Split the string `text` using shell-like syntax - - This avoids breaking single/double-quoted strings (e.g. containing + """ + Split the string `text` using shell-like syntax + + This avoids breaking single/double-quoted strings (e.g. containing strings with spaces). This function is almost equivalent to the shlex.split - function (see standard library `shlex`) except that it is supporting - unicode strings (shlex does not support unicode until Python 2.7.3).""" + function (see standard library `shlex`) except that it is supporting + unicode strings (shlex does not support unicode until Python 2.7.3). + """ assert is_text_string(text) # in case a QString is passed... pattern = r'(\s+|(?': return LooseVersion(actver) > LooseVersion(version) @@ -241,16 +383,18 @@ def is_module_installed(module_name, version=None, installed_version=None, interpreter=None): - """Return True if module *module_name* is installed - - If version is not None, checking module version + """ + Return True if module *module_name* is installed + + If version is not None, checking module version (module must have an attribute named '__version__') - + version may starts with =, >=, > or < to specify the exact requirement ; multiple conditions may be separated by ';' (e.g. '>=0.13;<1.0') - - interpreter: check if a module is installed with a given version - in a determined interpreter""" + + interpreter: check if a module is installed with a given version + in a determined interpreter + """ if interpreter: if not osp.isdir(TEMPDIR): os.mkdir(TEMPDIR) @@ -258,12 +402,14 @@ if osp.isfile(interpreter) and ('python' in interpreter): checkver = inspect.getsource(check_version) get_modver = inspect.getsource(get_module_version) + stable_ver = inspect.getsource(is_stable_version) ismod_inst = inspect.getsource(is_module_installed) fd, script = tempfile.mkstemp(suffix='.py', dir=TEMPDIR) with os.fdopen(fd, 'w') as f: f.write("# -*- coding: utf-8 -*-" + "\n\n") f.write("from distutils.version import LooseVersion" + "\n") f.write("import re" + "\n\n") + f.write(stable_ver + "\n") f.write(checkver + "\n") f.write(get_modver + "\n") f.write(ismod_inst + "\n") @@ -273,8 +419,8 @@ else: f.write("print(is_module_installed('%s'))" % module_name) try: - output, _err = subprocess.Popen([interpreter, script], - stdout=subprocess.PIPE).communicate() + proc = run_program(interpreter, [script]) + output, _err = proc.communicate() except subprocess.CalledProcessError: return True if output: # TODO: Check why output could be empty! @@ -317,10 +463,18 @@ return check_version(actver, version, symb) +def test_programs(): + assert find_program('git') + assert shell_split('-q -o -a') == ['-q', '-o', '-a'] + assert shell_split('-q "d:\\Python de xxxx\\t.txt" -o -a') == \ + ['-q', 'd:\\Python de xxxx\\t.txt', '-o', '-a'] + assert check_version('0.9.4-1', '0.9.4', '>=') + assert check_version('3.0.0rc1', '3.0.0', '<') + assert check_version('1.0', '1.0b2', '>') + assert is_module_installed('qtconsole', '>=4.0') + assert not is_module_installed('IPython', '>=1.0;<3.0') + assert is_module_installed('jedi', '>=0.7.0') + + if __name__ == '__main__': - print(find_program('hg')) - print(shell_split('-q -o -a')) - print(shell_split('-q "d:\\Python de xxxx\\t.txt" -o -a')) - print(is_module_installed('IPython', '>=0.12')) - print(is_module_installed('IPython', '>=0.13;<1.0')) - print(is_module_installed('jedi', '>=0.7.0')) + test_programs() diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/utils/qthelpers.py spyder-3.0.2+dfsg1/spyder/utils/qthelpers.py --- spyder-2.3.8+dfsg1/spyder/utils/qthelpers.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/utils/qthelpers.py 2016-11-16 16:40:01.000000000 +0100 @@ -1,29 +1,33 @@ # -*- coding: utf-8 -*- # -# Copyright © 2009-2011 Pierre Raybaut +# Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) +# (see spyder/__init__.py for details) """Qt utilities""" -from spyderlib.qt.QtGui import (QAction, QStyle, QWidget, QIcon, QApplication, - QLabel, QVBoxLayout, QHBoxLayout, QLineEdit, - QKeyEvent, QMenu, QKeySequence, QToolButton, - QPixmap) -from spyderlib.qt.QtCore import (SIGNAL, QObject, Qt, QLocale, QTranslator, - QLibraryInfo, QEvent) -from spyderlib.qt.compat import to_qvariant, from_qvariant - +# Standard library imports import os -import re import os.path as osp +import re import sys -# Local import -from spyderlib.baseconfig import get_image_path, running_in_mac_app -from spyderlib.guiconfig import get_shortcut -from spyderlib.utils import programs -from spyderlib.py3compat import is_text_string, to_text_string +# Third party imports +from qtpy.compat import to_qvariant, from_qvariant +from qtpy.QtCore import (QEvent, QLibraryInfo, QLocale, QObject, Qt, QTimer, + QTranslator, Signal, Slot) +from qtpy.QtGui import QIcon, QKeyEvent, QKeySequence, QPixmap +from qtpy.QtWidgets import (QAction, QApplication, QHBoxLayout, QLabel, + QLineEdit, QMenu, QStyle, QToolBar, QToolButton, + QVBoxLayout, QWidget) + +# Local imports +from spyder.config.base import get_image_path, running_in_mac_app +from spyder.config.gui import get_shortcut +from spyder.utils import programs +from spyder.utils import icon_manager as ima +from spyder.utils.icon_manager import get_icon, get_std_icon +from spyder.py3compat import is_text_string, to_text_string # Note: How to redirect a signal from widget *a* to widget *b* ? # ---- @@ -36,28 +40,6 @@ # lambda *args: self.emit(SIGNAL('option_changed'), *args)) -def get_icon(name, default=None, resample=False): - """Return image inside a QIcon object - default: default image name or icon - resample: if True, manually resample icon pixmaps for usual sizes - (16, 24, 32, 48, 96, 128, 256). This is recommended for QMainWindow icons - created from SVG images on non-Windows platforms due to a Qt bug (see - Issue 1314).""" - if default is None: - icon = QIcon(get_image_path(name)) - elif isinstance(default, QIcon): - icon_path = get_image_path(name, default=None) - icon = default if icon_path is None else QIcon(icon_path) - else: - icon = QIcon(get_image_path(name, default)) - if resample: - icon0 = QIcon() - for size in (16, 24, 32, 48, 96, 128, 256, 512): - icon0.addPixmap(icon.pixmap(size, size)) - return icon0 - else: - return icon - def get_image_label(name, default="not_found.png"): """Return image inside a QLabel object""" @@ -68,19 +50,26 @@ class MacApplication(QApplication): """Subclass to be able to open external files with our Mac app""" + sig_open_external_file = Signal(str) + def __init__(self, *args): QApplication.__init__(self, *args) def event(self, event): if event.type() == QEvent.FileOpen: fname = str(event.file()) - self.emit(SIGNAL('open_external_file(QString)'), fname) + self.sig_open_external_file.emit(fname) return QApplication.event(self, event) -def qapplication(translate=True): - """Return QApplication instance - Creates it if it doesn't already exist""" +def qapplication(translate=True, test_time=3): + """ + Return QApplication instance + Creates it if it doesn't already exist + + test_time: Time to maintain open the application when testing. It's given + in seconds + """ if running_in_mac_app(): SpyderApplication = MacApplication else: @@ -96,6 +85,12 @@ app.setApplicationName('Spyder') if translate: install_translator(app) + + test_ci = os.environ.get('TEST_CI_WIDGETS', None) + if test_ci is not None: + timer_shutdown = QTimer(app) + timer_shutdown.timeout.connect(app.quit) + timer_shutdown.start(test_time*1000) return app @@ -142,6 +137,7 @@ path = path[5:] else: path = path[7:] + path = path.replace('%5C' , os.sep) # Transforming backslashes if osp.exists(path): if extlist is None or osp.splitext(path)[1] in extlist: return path @@ -208,9 +204,9 @@ button.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) button.setAutoRaise(autoraise) if triggered is not None: - QObject.connect(button, SIGNAL('clicked()'), triggered) + button.clicked.connect(triggered) if toggled is not None: - QObject.connect(button, SIGNAL("toggled(bool)"), toggled) + button.toggled.connect(toggled) button.setCheckable(True) if shortcut is not None: button.setShortcut(shortcut) @@ -241,18 +237,16 @@ toggled=None, triggered=None, data=None, menurole=None, context=Qt.WindowShortcut): """Create a QAction""" - action = QAction(text, parent) + action = SpyderAction(text, parent) if triggered is not None: - parent.connect(action, SIGNAL("triggered()"), triggered) + action.triggered.connect(triggered) if toggled is not None: - parent.connect(action, SIGNAL("toggled(bool)"), toggled) + action.toggled.connect(toggled) action.setCheckable(True) if icon is not None: if is_text_string(icon): icon = get_icon(icon) action.setIcon(icon) - if shortcut is not None: - action.setShortcut(shortcut) if tip is not None: action.setToolTip(tip) action.setStatusTip(tip) @@ -260,10 +254,27 @@ action.setData(to_qvariant(data)) if menurole is not None: action.setMenuRole(menurole) - #TODO: Hard-code all shortcuts and choose context=Qt.WidgetShortcut - # (this will avoid calling shortcuts from another dockwidget - # since the context thing doesn't work quite well with these widgets) - action.setShortcutContext(context) + + # Workround for Mac because setting context=Qt.WidgetShortcut + # there doesn't have any effect + if sys.platform == 'darwin': + action._shown_shortcut = None + if context == Qt.WidgetShortcut: + if shortcut is not None: + action._shown_shortcut = shortcut + else: + # This is going to be filled by + # main.register_shortcut + action._shown_shortcut = 'missing' + else: + if shortcut is not None: + action.setShortcut(shortcut) + action.setShortcutContext(context) + else: + if shortcut is not None: + action.setShortcut(shortcut) + action.setShortcutContext(context) + return action @@ -274,7 +285,7 @@ def add_actions(target, actions, insert_before=None): - """Add actions to a menu""" + """Add actions to a QMenu or a QToolBar.""" previous_action = None target_actions = list(target.actions()) if target_actions: @@ -292,7 +303,9 @@ target.addMenu(action) else: target.insertMenu(insert_before, action) - elif isinstance(action, QAction): + elif isinstance(action, SpyderAction): + if isinstance(target, QMenu) or not isinstance(target, QToolBar): + action = action.no_icon_action if insert_before is None: target.addAction(action) else: @@ -312,8 +325,13 @@ def create_bookmark_action(parent, url, title, icon=None, shortcut=None): """Create bookmark action""" + + @Slot() + def open_url(): + return programs.start_file(url) + return create_action( parent, title, shortcut=shortcut, icon=icon, - triggered=lambda u=url: programs.start_file(u) ) + triggered=open_url) def create_module_bookmark_actions(parent, bookmarks): @@ -378,11 +396,11 @@ else: dialog.show() self.dialogs[id(dialog)] = dialog - self.connect(dialog, SIGNAL('accepted()'), - lambda eid=id(dialog): self.dialog_finished(eid)) - self.connect(dialog, SIGNAL('rejected()'), - lambda eid=id(dialog): self.dialog_finished(eid)) - + dialog.accepted.connect( + lambda eid=id(dialog): self.dialog_finished(eid)) + dialog.rejected.connect( + lambda eid=id(dialog): self.dialog_finished(eid)) + def dialog_finished(self, dialog_id): """Manage non-modal dialog boxes""" return self.dialogs.pop(dialog_id) @@ -393,24 +411,54 @@ dlg.reject() -def get_std_icon(name, size=None): - """Get standard platform icon - Call 'show_std_icons()' for details""" - if not name.startswith('SP_'): - name = 'SP_'+name - icon = QWidget().style().standardIcon( getattr(QStyle, name) ) - if size is None: - return icon - else: - return QIcon( icon.pixmap(size, size) ) - - def get_filetype_icon(fname): """Return file type icon""" ext = osp.splitext(fname)[1] if ext.startswith('.'): ext = ext[1:] - return get_icon( "%s.png" % ext, get_std_icon('FileIcon') ) + return get_icon( "%s.png" % ext, ima.icon('FileIcon') ) + + +class SpyderAction(QAction): + """Spyder QAction class wrapper to handle cross platform patches.""" + + def __init__(self, *args, **kwargs): + """Spyder QAction class wrapper to handle cross platform patches.""" + super(SpyderAction, self).__init__(*args, **kwargs) + self._action_no_icon = None + + if sys.platform == 'darwin': + self._action_no_icon = QAction(*args, **kwargs) + self._action_no_icon.setIcon(QIcon()) + self._action_no_icon.triggered.connect(self.triggered) + self._action_no_icon.toggled.connect(self.toggled) + self._action_no_icon.changed.connect(self.changed) + self._action_no_icon.hovered.connect(self.hovered) + else: + self._action_no_icon = self + + def __getattribute__(self, name): + """Intercept method calls and apply to both actions, except signals.""" + attr = super(SpyderAction, self).__getattribute__(name) + + if hasattr(attr, '__call__') and name not in ['triggered', 'toggled', + 'changed', 'hovered']: + def newfunc(*args, **kwargs): + result = attr(*args, **kwargs) + if name not in ['setIcon']: + action_no_icon = self.__dict__['_action_no_icon'] + attr_no_icon = super(QAction, + action_no_icon).__getattribute__(name) + attr_no_icon(*args, **kwargs) + return result + return newfunc + else: + return attr + + @property + def no_icon_action(self): + """Return the action without an Icon.""" + return self._action_no_icon class ShowStdIcons(QWidget): diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/utils/site/__init__.py spyder-3.0.2+dfsg1/spyder/utils/site/__init__.py --- spyder-2.3.8+dfsg1/spyder/utils/site/__init__.py 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/utils/site/__init__.py 2016-10-25 02:05:23.000000000 +0200 @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# +# Copyright © Spyder Project Contributors +# Licensed under the terms of the MIT License +# (see spyder/__init__.py for details) + +""" +spyder.utils.site +================= + +Site packages for Spyder consoles + +NOTE: This package shouldn't be imported at **any** place. + It's only used to set additional functionality for + our consoles. +""" diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/utils/site/osx_app_site.py spyder-3.0.2+dfsg1/spyder/utils/site/osx_app_site.py --- spyder-2.3.8+dfsg1/spyder/utils/site/osx_app_site.py 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/utils/site/osx_app_site.py 2016-10-25 02:05:23.000000000 +0200 @@ -0,0 +1,163 @@ +# +# IMPORTANT NOTE: Don't add a coding line here! It's not necessary for +# site files +# +# Spyder's MacOS X App site.py additions +# +# It includes missing variables and paths that are not added by +# py2app to its own site.py +# +# These functions were taken verbatim from Python 2.7.3 site.py +# + +import sys +import os +try: + import __builtin__ as builtins +except ImportError: + # Python 3 + import builtins + +# for distutils.commands.install +# These values are initialized by the getuserbase() and getusersitepackages() +# functions. +USER_SITE = None +USER_BASE = None + + +def getuserbase(): + """Returns the `user base` directory path. + + The `user base` directory can be used to store data. If the global + variable ``USER_BASE`` is not initialized yet, this function will also set + it. + """ + global USER_BASE + if USER_BASE is not None: + return USER_BASE + from sysconfig import get_config_var + USER_BASE = get_config_var('userbase') + return USER_BASE + +def getusersitepackages(): + """Returns the user-specific site-packages directory path. + + If the global variable ``USER_SITE`` is not initialized yet, this + function will also set it. + """ + global USER_SITE + user_base = getuserbase() # this will also set USER_BASE + + if USER_SITE is not None: + return USER_SITE + + from sysconfig import get_path + + if sys.platform == 'darwin': + from sysconfig import get_config_var + if get_config_var('PYTHONFRAMEWORK'): + USER_SITE = get_path('purelib', 'osx_framework_user') + return USER_SITE + + USER_SITE = get_path('purelib', '%s_user' % os.name) + return USER_SITE + + +class _Printer(object): + """interactive prompt objects for printing the license text, a list of + contributors and the copyright notice.""" + + MAXLINES = 23 + + def __init__(self, name, data, files=(), dirs=()): + self.__name = name + self.__data = data + self.__files = files + self.__dirs = dirs + self.__lines = None + + def __setup(self): + if self.__lines: + return + data = None + for dir in self.__dirs: + for filename in self.__files: + filename = os.path.join(dir, filename) + try: + fp = open(filename, "rU") + data = fp.read() + fp.close() + break + except IOError: + pass + if data: + break + if not data: + data = self.__data + self.__lines = data.split('\n') + self.__linecnt = len(self.__lines) + + def __repr__(self): + self.__setup() + if len(self.__lines) <= self.MAXLINES: + return "\n".join(self.__lines) + else: + return "Type %s() to see the full %s text" % ((self.__name,)*2) + + def __call__(self): + self.__setup() + prompt = 'Hit Return for more, or q (and Return) to quit: ' + lineno = 0 + while 1: + try: + for i in range(lineno, lineno + self.MAXLINES): + print(self.__lines[i]) + except IndexError: + break + else: + lineno += self.MAXLINES + key = None + while key is None: + try: + key = raw_input(prompt) + except NameError: + # Python 3 + key = input(prompt) + if key not in ('', 'q'): + key = None + if key == 'q': + break + +def setcopyright(): + """Set 'copyright' and 'credits' in builtins""" + builtins.copyright = _Printer("copyright", sys.copyright) + if sys.platform[:4] == 'java': + builtins.credits = _Printer( + "credits", + "Jython is maintained by the Jython developers (www.jython.org).") + else: + builtins.credits = _Printer("credits", """\ + Thanks to CWI, CNRI, BeOpen.com, Zope Corporation and a cast of thousands + for supporting Python development. See www.python.org for more information.""") + here = os.path.dirname(os.__file__) + builtins.license = _Printer( + "license", "See http://www.python.org/%.3s/license.html" % sys.version, + ["LICENSE.txt", "LICENSE"], + [os.path.join(here, os.pardir), here, os.curdir]) + + +class _Helper(object): + """Define the builtin 'help'. + This is a wrapper around pydoc.help (with a twist). + + """ + + def __repr__(self): + return "Type help() for interactive help, " \ + "or help(object) for help about object." + def __call__(self, *args, **kwds): + import pydoc + return pydoc.help(*args, **kwds) + +def sethelper(): + builtins.help = _Helper() diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/utils/site/sitecustomize.py spyder-3.0.2+dfsg1/spyder/utils/site/sitecustomize.py --- spyder-2.3.8+dfsg1/spyder/utils/site/sitecustomize.py 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/utils/site/sitecustomize.py 2016-11-16 16:40:01.000000000 +0100 @@ -0,0 +1,947 @@ +# +# Copyright (c) Spyder Project Contributors) +# Licensed under the terms of the MIT License) +# (see spyder/__init__.py for details) +# +# IMPORTANT NOTE: Don't add a coding line here! It's not necessary for +# site files +# +# Spyder consoles sitecustomize +# + +import sys +import os +import os.path as osp +import pdb +import bdb +import time +import traceback +import shlex + + +PY2 = sys.version[0] == '2' + + +#============================================================================== +# sys.argv can be missing when Python is embedded, taking care of it. +# Fixes Issue 1473 and other crazy crashes with IPython 0.13 trying to +# access it. +#============================================================================== +if not hasattr(sys, 'argv'): + sys.argv = [''] + + +#============================================================================== +# Main constants +#============================================================================== +IS_IPYKERNEL = os.environ.get("IPYTHON_KERNEL", "").lower() == "true" +IS_EXT_INTERPRETER = os.environ.get('EXTERNAL_INTERPRETER', '').lower() == "true" + + +#============================================================================== +# Important Note: +# +# We avoid importing spyder here, so we are handling Python 3 compatiblity +# by hand. +#============================================================================== +def _print(*objects, **options): + end = options.get('end', '\n') + file = options.get('file', sys.stdout) + sep = options.get('sep', ' ') + string = sep.join([str(obj) for obj in objects]) + if not PY2: + # Python 3 + local_dict = {} + exec('printf = print', local_dict) # to avoid syntax error in Python 2 + local_dict['printf'](string, file=file, end=end, sep=sep) + else: + # Python 2 + if end: + print >>file, string + else: + print >>file, string, + + +#============================================================================== +# Execfile functions +# +# The definitions for Python 2 on Windows were taken from the IPython project +# Copyright (C) The IPython Development Team +# Distributed under the terms of the modified BSD license +#============================================================================== +try: + # Python 2 + import __builtin__ as builtins + if os.name == 'nt': + def encode(u): + return u.encode('utf8', 'replace') + def execfile(fname, glob=None, loc=None): + loc = loc if (loc is not None) else glob + scripttext = builtins.open(fname).read()+ '\n' + # compile converts unicode filename to str assuming + # ascii. Let's do the conversion before calling compile + if isinstance(fname, unicode): + filename = encode(fname) + else: + filename = fname + exec(compile(scripttext, filename, 'exec'), glob, loc) + else: + def execfile(fname, *where): + if isinstance(fname, unicode): + filename = fname.encode(sys.getfilesystemencoding()) + else: + filename = fname + builtins.execfile(filename, *where) +except ImportError: + # Python 3 + import builtins + basestring = (str,) + def execfile(filename, namespace): + # Open a source file correctly, whatever its encoding is + with open(filename, 'rb') as f: + exec(compile(f.read(), filename, 'exec'), namespace) + + +#============================================================================== +# Colorization of sys.stderr (standard Python interpreter) +#============================================================================== +if os.environ.get("COLORIZE_SYS_STDERR", "").lower() == "true": + class StderrProxy(object): + """Proxy to sys.stderr file object overriding only the `write` method + to provide red colorization for the whole stream, and blue-underlined + for traceback file links""" + def __init__(self): + self.old_stderr = sys.stderr + self.__buffer = '' + sys.stderr = self + + def __getattr__(self, name): + return getattr(self.old_stderr, name) + + def write(self, text): + if os.name == 'nt' and '\n' not in text: + self.__buffer += text + return + for text in (self.__buffer+text).splitlines(True): + if text.startswith(' File') \ + and not text.startswith(' File "<'): + # Show error links in blue underlined text + colored_text = ' '+'\x1b[4;34m'+text[2:]+'\x1b[0m' + else: + # Show error messages in red + colored_text = '\x1b[31m'+text+'\x1b[0m' + self.old_stderr.write(colored_text) + self.__buffer = '' + + stderrproxy = StderrProxy() + + +#============================================================================== +# Prepending this spyder package's path to sys.path to be sure +# that another version of spyder won't be imported instead: +#============================================================================== +spyder_path = osp.dirname(__file__) +while not osp.isdir(osp.join(spyder_path, 'spyder')): + spyder_path = osp.abspath(osp.join(spyder_path, os.pardir)) +if not spyder_path.startswith(sys.prefix): + # Spyder is not installed: moving its parent directory to the top of + # sys.path to be sure that this spyder package will be imported in + # the remote process (instead of another installed version of Spyder) + while spyder_path in sys.path: + sys.path.remove(spyder_path) + sys.path.insert(0, spyder_path) +os.environ['SPYDER_PARENT_DIR'] = spyder_path + + +#============================================================================== +# Setting console encoding (otherwise Python does not recognize encoding) +# for Windows platforms +#============================================================================== +if os.name == 'nt' and PY2: + try: + import locale, ctypes + _t, _cp = locale.getdefaultlocale('LANG') + try: + _cp = int(_cp[2:]) + ctypes.windll.kernel32.SetConsoleCP(_cp) + ctypes.windll.kernel32.SetConsoleOutputCP(_cp) + except (ValueError, TypeError): + # Code page number in locale is not valid + pass + except ImportError: + pass + + +#============================================================================== +# Settings for our MacOs X app +#============================================================================== +if sys.platform == 'darwin': + from spyder.config.base import MAC_APP_NAME + if MAC_APP_NAME in __file__: + if IS_EXT_INTERPRETER: + # Add a minimal library (with spyder) at the end of sys.path to + # be able to connect our monitor to the external console + py_ver = '%s.%s' % (sys.version_info[0], sys.version_info[1]) + app_pythonpath = '%s/Contents/Resources/lib/python%s' % (MAC_APP_NAME, + py_ver) + full_pythonpath = [p for p in sys.path if p.endswith(app_pythonpath)] + if full_pythonpath: + sys.path.remove(full_pythonpath[0]) + sys.path.append(full_pythonpath[0] + osp.sep + 'minimal-lib') + else: + # Add missing variables and methods to the app's site module + import site + import osx_app_site + osx_app_site.setcopyright() + osx_app_site.sethelper() + site._Printer = osx_app_site._Printer + site.USER_BASE = osx_app_site.getuserbase() + site.USER_SITE = osx_app_site.getusersitepackages() + + +#============================================================================== +# Importing user's sitecustomize +#============================================================================== +try: + import sitecustomize #analysis:ignore +except ImportError: + pass + + +#============================================================================== +# Add default filesystem encoding on Linux to avoid an error with +# Matplotlib 1.5 in Python 2 (Fixes Issue 2793) +#============================================================================== +if PY2 and sys.platform.startswith('linux'): + def _getfilesystemencoding_wrapper(): + return 'utf-8' + + sys.getfilesystemencoding = _getfilesystemencoding_wrapper + + +#============================================================================== +# Set PyQt API to #2 +#============================================================================== +if os.environ["QT_API"] == 'pyqt': + try: + import sip + for qtype in ('QString', 'QVariant', 'QDate', 'QDateTime', + 'QTextStream', 'QTime', 'QUrl'): + sip.setapi(qtype, 2) + except: + pass + + +#============================================================================== +# Importing matplotlib before creating the monitor. +# This prevents a kernel crash with the inline backend in our IPython +# consoles on Linux and Python 3 (Fixes Issue 2257) +#============================================================================== +try: + import matplotlib +except ImportError: + matplotlib = None # analysis:ignore + + +#============================================================================== +# Monitor-based functionality +#============================================================================== +if IS_IPYKERNEL or os.environ.get('SPYDER_SHELL_ID') is None: + monitor = None +else: + from spyder.widgets.externalshell.monitor import Monitor + monitor = Monitor("127.0.0.1", + int(os.environ['SPYDER_I_PORT']), + int(os.environ['SPYDER_N_PORT']), + os.environ['SPYDER_SHELL_ID'], + float(os.environ['SPYDER_AR_TIMEOUT']), + os.environ["SPYDER_AR_STATE"].lower() == "true") + monitor.start() + + def open_in_spyder(source, lineno=1): + """ + Open a source file in Spyder's editor (it could be a filename or a + Python module/package). + + If you want to use IPython's %edit use %ed instead + """ + try: + source = sys.modules[source] + except KeyError: + source = source + if not isinstance(source, basestring): + try: + source = source.__file__ + except AttributeError: + raise ValueError("source argument must be either " + "a string or a module object") + if source.endswith('.pyc'): + source = source[:-1] + source = osp.abspath(source) + if osp.exists(source): + monitor.notify_open_file(source, lineno=lineno) + else: + _print("Can't open file %s" % source, file=sys.stderr) + builtins.open_in_spyder = open_in_spyder + + # Our own input hook, monitor based and for Windows only + if os.name == 'nt' and matplotlib and not IS_IPYKERNEL: + # Qt imports + if os.environ["QT_API"] == 'pyqt5': + from PyQt5 import QtCore + from PyQt5 import QtWidgets + elif os.environ["QT_API"] == 'pyqt': + from PyQt4 import QtCore # analysis:ignore + from PyQt4 import QtGui as QtWidgets + elif os.environ["QT_API"] == 'pyside': + from PySide import QtCore # analysis:ignore + from PySide import QtGui as QtWidgets + + def qt_nt_inputhook(): + """Qt input hook for Windows + + This input hook wait for available stdin data (notified by + ExternalPythonShell through the monitor's inputhook_flag + attribute), and in the meantime it processes Qt events. + """ + # Refreshing variable explorer, except on first input hook call: + # (otherwise, on slow machines, this may freeze Spyder) + monitor.refresh_from_inputhook() + + # NOTE: This is making the inputhoook to fail completely!! + # That's why it's commented. + #try: + # This call fails for Python without readline support + # (or on Windows platforms) when PyOS_InputHook is called + # for the second consecutive time, because the 100-bytes + # stdin buffer is full. + # For more details, see the `PyOS_StdioReadline` function + # in Python source code (Parser/myreadline.c) + # sys.stdin.tell() + #except IOError: + # return 0 + + # Input hook + app = QtCore.QCoreApplication.instance() + if app is None: + app = QtWidgets.QApplication([" "]) + if app and app.thread() is QtCore.QThread.currentThread(): + try: + timer = QtCore.QTimer() + timer.timeout.connect(app.quit) + monitor.toggle_inputhook_flag(False) + while not monitor.inputhook_flag: + timer.start(50) + QtCore.QCoreApplication.exec_() + timer.stop() + except KeyboardInterrupt: + _print("\nKeyboardInterrupt - Press Enter for new prompt") + + # Socket-based alternative: + #socket = QtNetwork.QLocalSocket() + #socket.connectToServer(os.environ['SPYDER_SHELL_ID']) + #socket.waitForConnected(-1) + #while not socket.waitForReadyRead(10): + # timer.start(50) + # QtCore.QCoreApplication.exec_() + # timer.stop() + #socket.read(3) + #socket.disconnectFromServer() + return 0 + + +#============================================================================== +# Matplotlib settings +#============================================================================== +if matplotlib is not None: + if not IS_IPYKERNEL: + mpl_backend = os.environ.get("SPY_MPL_BACKEND", "") + mpl_ion = os.environ.get("MATPLOTLIB_ION", "") + + # Setting no backend if the user asks for it + if not mpl_backend or mpl_backend.lower() == 'none': + mpl_backend = "" + + # Set backend automatically + if mpl_backend.lower() == 'automatic': + if not IS_EXT_INTERPRETER: + if os.environ["QT_API"] == 'pyqt5': + mpl_backend = 'Qt5Agg' + else: + mpl_backend = 'Qt4Agg' + else: + # Test for backend libraries on external interpreters + def set_mpl_backend(backend): + mod, bend, qt_api = backend + try: + if mod: + __import__(mod) + if qt_api and (os.environ["QT_API"] != qt_api): + return None + else: + matplotlib.use(bend) + return bend + except (ImportError, ValueError): + return None + + backends = [('PyQt5', 'Qt5Agg', 'pyqt5'), + ('PyQt4', 'Qt4Agg', 'pyqt'), + ('PySide', 'Qt4Agg', 'pyqt')] + if not os.name == 'nt': + backends.append( ('_tkinter', 'TkAgg', None) ) + + for b in backends: + mpl_backend = set_mpl_backend(b) + if mpl_backend: + break + + if not mpl_backend: + _print("NOTE: No suitable Matplotlib backend was found!\n" + " You won't be able to create plots\n") + + # To have mpl docstrings as rst + matplotlib.rcParams['docstring.hardcopy'] = True + + # Activate interactive mode when needed + if mpl_ion.lower() == "true": + matplotlib.rcParams['interactive'] = True + + from spyder.utils import inputhooks + if mpl_backend: + import ctypes + + # Grab QT_API + qt_api = os.environ["QT_API"] + + # Setting the user defined backend + if not IS_EXT_INTERPRETER: + matplotlib.use(mpl_backend) + + # Setting the right input hook according to mpl_backend, + # IMPORTANT NOTE: Don't try to abstract the steps to set a PyOS + # input hook callback in a function. It will **crash** the + # interpreter!! + if (mpl_backend == "Qt4Agg" or mpl_backend == "Qt5Agg") and \ + os.name == 'nt' and monitor is not None: + # Removing PyQt4 input hook which is not working well on + # Windows since opening a subprocess does not attach a real + # console to it (with keyboard events...) + if qt_api == 'pyqt' or qt_api == 'pyqt5': + inputhooks.remove_pyqt_inputhook() + # Using our own input hook + # NOTE: it's not working correctly for some configurations + # (See issue 1831) + callback = inputhooks.set_pyft_callback(qt_nt_inputhook) + pyos_ih = inputhooks.get_pyos_inputhook() + pyos_ih.value = ctypes.cast(callback, ctypes.c_void_p).value + elif mpl_backend == "Qt4Agg" and qt_api == 'pyside': + # PySide doesn't have an input hook, so we need to install one + # to be able to show plots + # Note: This only works well for Posix systems + callback = inputhooks.set_pyft_callback(inputhooks.qt4) + pyos_ih = inputhooks.get_pyos_inputhook() + pyos_ih.value = ctypes.cast(callback, ctypes.c_void_p).value + elif (mpl_backend != "Qt4Agg" and qt_api == 'pyqt') \ + or (mpl_backend != "Qt5Agg" and qt_api == 'pyqt5'): + # Matplotlib backends install their own input hooks, so we + # need to remove the PyQt one to make them work + inputhooks.remove_pyqt_inputhook() + else: + inputhooks.remove_pyqt_inputhook() + else: + # To have mpl docstrings as rst + matplotlib.rcParams['docstring.hardcopy'] = True + + +#============================================================================== +# IPython kernel adjustments +#============================================================================== +if IS_IPYKERNEL: + # Use ipydb as the debugger to patch on IPython consoles + from IPython.core.debugger import Pdb as ipyPdb + pdb.Pdb = ipyPdb + + # Patch unittest.main so that errors are printed directly in the console. + # See http://comments.gmane.org/gmane.comp.python.ipython.devel/10557 + # Fixes Issue 1370 + import unittest + from unittest import TestProgram + class IPyTesProgram(TestProgram): + def __init__(self, *args, **kwargs): + test_runner = unittest.TextTestRunner(stream=sys.stderr) + kwargs['testRunner'] = kwargs.pop('testRunner', test_runner) + kwargs['exit'] = False + TestProgram.__init__(self, *args, **kwargs) + unittest.main = IPyTesProgram + + +#============================================================================== +# Pandas adjustments +#============================================================================== +try: + # Make Pandas recognize our Jupyter consoles as proper qtconsoles + # Fixes Issue 2015 + def in_qtconsole(): + return True + import pandas as pd + pd.core.common.in_qtconsole = in_qtconsole + + # Set Pandas output encoding + pd.options.display.encoding = 'utf-8' + + # Filter warning that appears for DataFrames with np.nan values + # Example: + # >>> import pandas as pd, numpy as np + # >>> pd.Series([np.nan,np.nan,np.nan],index=[1,2,3]) + # Fixes Issue 2991 + import warnings + # For 0.18- + warnings.filterwarnings(action='ignore', category=RuntimeWarning, + module='pandas.core.format', + message=".*invalid value encountered in.*") + # For 0.18.1+ + warnings.filterwarnings(action='ignore', category=RuntimeWarning, + module='pandas.formats.format', + message=".*invalid value encountered in.*") +except (ImportError, AttributeError): + pass + + +#============================================================================== +# Pdb adjustments +#============================================================================== +class SpyderPdb(pdb.Pdb): + send_initial_notification = True + + def set_spyder_breakpoints(self): + self.clear_all_breaks() + #------Really deleting all breakpoints: + for bp in bdb.Breakpoint.bpbynumber: + if bp: + bp.deleteMe() + bdb.Breakpoint.next = 1 + bdb.Breakpoint.bplist = {} + bdb.Breakpoint.bpbynumber = [None] + #------ + from spyder.config.main import CONF + CONF.load_from_ini() + if CONF.get('run', 'breakpoints/enabled', True): + breakpoints = CONF.get('run', 'breakpoints', {}) + i = 0 + for fname, data in list(breakpoints.items()): + for linenumber, condition in data: + i += 1 + self.set_break(self.canonic(fname), linenumber, + cond=condition) + + def notify_spyder(self, frame): + if not frame: + return + fname = self.canonic(frame.f_code.co_filename) + if PY2: + try: + fname = unicode(fname, "utf-8") + except TypeError: + pass + lineno = frame.f_lineno + if isinstance(fname, basestring) and isinstance(lineno, int): + if osp.isfile(fname): + if IS_IPYKERNEL: + from IPython.core.getipython import get_ipython + ipython_shell = get_ipython() + if ipython_shell: + step = dict(fname=fname, lineno=lineno) + ipython_shell.kernel._pdb_step = step + elif monitor is not None: + monitor.notify_pdb_step(fname, lineno) + time.sleep(0.1) + +pdb.Pdb = SpyderPdb + +#XXX: I know, this function is now also implemented as is in utils/misc.py but +# I'm kind of reluctant to import spyder in sitecustomize, even if this +# import is very clean. +def monkeypatch_method(cls, patch_name): + # This function's code was inspired from the following thread: + # "[Python-Dev] Monkeypatching idioms -- elegant or ugly?" + # by Robert Brewer + # (Tue Jan 15 19:13:25 CET 2008) + """ + Add the decorated method to the given class; replace as needed. + + If the named method already exists on the given class, it will + be replaced, and a reference to the old method is created as + cls._old. If the "_old__" attribute + already exists, KeyError is raised. + """ + def decorator(func): + fname = func.__name__ + old_func = getattr(cls, fname, None) + if old_func is not None: + # Add the old func to a list of old funcs. + old_ref = "_old_%s_%s" % (patch_name, fname) + #print(old_ref, old_func) + old_attr = getattr(cls, old_ref, None) + if old_attr is None: + setattr(cls, old_ref, old_func) + else: + raise KeyError("%s.%s already exists." + % (cls.__name__, old_ref)) + setattr(cls, fname, func) + return func + return decorator + +@monkeypatch_method(pdb.Pdb, 'Pdb') +def user_return(self, frame, return_value): + """This function is called when a return trap is set here.""" + # This is useful when debugging in an active interpreter (otherwise, + # the debugger will stop before reaching the target file) + if self._wait_for_mainpyfile: + if (self.mainpyfile != self.canonic(frame.f_code.co_filename) + or frame.f_lineno<= 0): + return + self._wait_for_mainpyfile = 0 + self._old_Pdb_user_return(frame, return_value) + +@monkeypatch_method(pdb.Pdb, 'Pdb') +def interaction(self, frame, traceback): + self.setup(frame, traceback) + if self.send_initial_notification: + self.notify_spyder(frame) #-----Spyder-specific----------------------- + self.print_stack_entry(self.stack[self.curindex]) + self.cmdloop() + self.forget() + +@monkeypatch_method(pdb.Pdb, 'Pdb') +def reset(self): + self._old_Pdb_reset() + if IS_IPYKERNEL: + from IPython.core.getipython import get_ipython + ipython_shell = get_ipython() + if ipython_shell: + ipython_shell.kernel._register_pdb_session(self) + elif monitor is not None: + monitor.register_pdb_session(self) + self.set_spyder_breakpoints() + +#XXX: notify spyder on any pdb command (is that good or too lazy? i.e. is more +# specific behaviour desired?) +@monkeypatch_method(pdb.Pdb, 'Pdb') +def postcmd(self, stop, line): + self.notify_spyder(self.curframe) + return self._old_Pdb_postcmd(stop, line) + +# Breakpoints don't work for files with non-ascii chars in Python 2 +# Fixes Issue 1484 +if PY2: + @monkeypatch_method(pdb.Pdb, 'Pdb') + def break_here(self, frame): + from bdb import effective + filename = self.canonic(frame.f_code.co_filename) + try: + filename = unicode(filename, "utf-8") + except TypeError: + pass + if not filename in self.breaks: + return False + lineno = frame.f_lineno + if not lineno in self.breaks[filename]: + # The line itself has no breakpoint, but maybe the line is the + # first line of a function with breakpoint set by function name. + lineno = frame.f_code.co_firstlineno + if not lineno in self.breaks[filename]: + return False + + # flag says ok to delete temp. bp + (bp, flag) = effective(filename, lineno, frame) + if bp: + self.currentbp = bp.number + if (flag and bp.temporary): + self.do_clear(str(bp.number)) + return True + else: + return False + + +#============================================================================== +# Restoring (almost) original sys.path: +# +# NOTE: do not remove spyder_path from sys.path because if Spyder has been +# installed using python setup.py install, then this could remove the +# 'site-packages' directory from sys.path! +#============================================================================== +try: + sys.path.remove(osp.join(spyder_path, "spyder", "widgets", + "externalshell")) +except ValueError: + pass + + +#============================================================================== +# User module reloader +#============================================================================== +class UserModuleReloader(object): + """ + User Module Reloader (UMR) aims at deleting user modules + to force Python to deeply reload them during import + + pathlist [list]: blacklist in terms of module path + namelist [list]: blacklist in terms of module name + """ + def __init__(self, namelist=None, pathlist=None): + if namelist is None: + namelist = [] + spy_modules = ['sitecustomize', 'spyder', 'spyderplugins'] + mpl_modules = ['matplotlib', 'tkinter', 'Tkinter'] + self.namelist = namelist + spy_modules + mpl_modules + + if pathlist is None: + pathlist = [] + self.pathlist = pathlist + self.previous_modules = list(sys.modules.keys()) + + def is_module_blacklisted(self, modname, modpath): + for path in [sys.prefix]+self.pathlist: + if modpath.startswith(path): + return True + else: + return set(modname.split('.')) & set(self.namelist) + + def run(self, verbose=False): + """ + Del user modules to force Python to deeply reload them + + Do not del modules which are considered as system modules, i.e. + modules installed in subdirectories of Python interpreter's binary + Do not del C modules + """ + log = [] + for modname, module in list(sys.modules.items()): + if modname not in self.previous_modules: + modpath = getattr(module, '__file__', None) + if modpath is None: + # *module* is a C module that is statically linked into the + # interpreter. There is no way to know its path, so we + # choose to ignore it. + continue + if not self.is_module_blacklisted(modname, modpath): + log.append(modname) + del sys.modules[modname] + if verbose and log: + _print("\x1b[4;33m%s\x1b[24m%s\x1b[0m"\ + % ("Reloaded modules", ": "+", ".join(log))) + +__umr__ = None + + +#============================================================================== +# Handle Post Mortem Debugging and Traceback Linkage to Spyder +#============================================================================== +def clear_post_mortem(): + """ + Remove the post mortem excepthook and replace with a standard one. + """ + if IS_IPYKERNEL: + from IPython.core.getipython import get_ipython + ipython_shell = get_ipython() + if ipython_shell: + ipython_shell.set_custom_exc((), None) + else: + sys.excepthook = sys.__excepthook__ + + +def post_mortem_excepthook(type, value, tb): + """ + For post mortem exception handling, print a banner and enable post + mortem debugging. + """ + clear_post_mortem() + if IS_IPYKERNEL: + from IPython.core.getipython import get_ipython + ipython_shell = get_ipython() + ipython_shell.showtraceback((type, value, tb)) + p = pdb.Pdb(ipython_shell.colors) + else: + traceback.print_exception(type, value, tb, file=sys.stderr) + p = pdb.Pdb() + + if not type == SyntaxError: + # wait for stderr to print (stderr.flush does not work in this case) + time.sleep(0.1) + _print('*' * 40) + _print('Entering post mortem debugging...') + _print('*' * 40) + # add ability to move between frames + p.send_initial_notification = False + p.reset() + frame = tb.tb_frame + prev = frame + while frame.f_back: + prev = frame + frame = frame.f_back + frame = prev + # wait for stdout to print + time.sleep(0.1) + p.interaction(frame, tb) + + +def set_post_mortem(): + """ + Enable the post mortem debugging excepthook. + """ + if IS_IPYKERNEL: + from IPython.core.getipython import get_ipython + def ipython_post_mortem_debug(shell, etype, evalue, tb, + tb_offset=None): + post_mortem_excepthook(etype, evalue, tb) + ipython_shell = get_ipython() + ipython_shell.set_custom_exc((Exception,), ipython_post_mortem_debug) + else: + sys.excepthook = post_mortem_excepthook + +# Add post mortem debugging if requested and in a dedicated interpreter +# existing interpreters use "runfile" below +if "SPYDER_EXCEPTHOOK" in os.environ: + set_post_mortem() + + +#============================================================================== +# runfile and debugfile commands +#============================================================================== +def _get_globals(): + """Return current Python interpreter globals namespace""" + from __main__ import __dict__ as namespace + shell = namespace.get('__ipythonshell__') + if shell is not None and hasattr(shell, 'user_ns'): + # IPython 0.13+ kernel + return shell.user_ns + else: + # Python interpreter + return namespace + return namespace + + +def runfile(filename, args=None, wdir=None, namespace=None, post_mortem=False): + """ + Run filename + args: command line arguments (string) + wdir: working directory + post_mortem: boolean, whether to enter post-mortem mode on error + """ + try: + filename = filename.decode('utf-8') + except (UnicodeError, TypeError, AttributeError): + # UnicodeError, TypeError --> eventually raised in Python 2 + # AttributeError --> systematically raised in Python 3 + pass + global __umr__ + if os.environ.get("UMR_ENABLED", "").lower() == "true": + if __umr__ is None: + namelist = os.environ.get("UMR_NAMELIST", None) + if namelist is not None: + namelist = namelist.split(',') + __umr__ = UserModuleReloader(namelist=namelist) + else: + verbose = os.environ.get("UMR_VERBOSE", "").lower() == "true" + __umr__.run(verbose=verbose) + if args is not None and not isinstance(args, basestring): + raise TypeError("expected a character buffer object") + if namespace is None: + namespace = _get_globals() + namespace['__file__'] = filename + sys.argv = [filename] + if args is not None: + for arg in shlex.split(args): + sys.argv.append(arg) + if wdir is not None: + try: + wdir = wdir.decode('utf-8') + except (UnicodeError, TypeError, AttributeError): + # UnicodeError, TypeError --> eventually raised in Python 2 + # AttributeError --> systematically raised in Python 3 + pass + os.chdir(wdir) + if post_mortem: + set_post_mortem() + execfile(filename, namespace) + clear_post_mortem() + sys.argv = [''] + namespace.pop('__file__') + +builtins.runfile = runfile + + +def debugfile(filename, args=None, wdir=None, post_mortem=False): + """ + Debug filename + args: command line arguments (string) + wdir: working directory + post_mortem: boolean, included for compatiblity with runfile + """ + debugger = pdb.Pdb() + filename = debugger.canonic(filename) + debugger._wait_for_mainpyfile = 1 + debugger.mainpyfile = filename + debugger._user_requested_quit = 0 + if os.name == 'nt': + filename = filename.replace('\\', '/') + debugger.run("runfile(%r, args=%r, wdir=%r)" % (filename, args, wdir)) + +builtins.debugfile = debugfile + + +#============================================================================== +# Evaluate external commands +#============================================================================== +def evalsc(command): + """Evaluate special commands + (analog to IPython's magic commands but far less powerful/complete)""" + assert command.startswith('%') + from spyder.utils import programs + + namespace = _get_globals() + command = command[1:].strip() # Remove leading % + + import re + clear_match = re.match(r"^clear ([a-zA-Z0-9_, ]+)", command) + cd_match = re.match(r"^cd \"?\'?([a-zA-Z0-9_\ \:\\\/\.]+)", command) + + if cd_match: + os.chdir(eval('r"%s"' % cd_match.groups()[0].strip())) + elif clear_match: + varnames = clear_match.groups()[0].replace(' ', '').split(',') + for varname in varnames: + try: + namespace.pop(varname) + except KeyError: + pass + elif command in ('cd', 'pwd'): + try: + _print(os.getcwdu()) + except AttributeError: + _print(os.getcwd()) + elif command == 'ls': + if os.name == 'nt': + programs.run_shell_command('dir') + _print('\n') + else: + programs.run_shell_command('ls') + _print('\n') + elif command == 'scientific': + from spyder.config import base + execfile(base.SCIENTIFIC_STARTUP, namespace) + else: + raise NotImplementedError("Unsupported command: '%s'" % command) + +builtins.evalsc = evalsc + + +#============================================================================== +# Restoring original PYTHONPATH +#============================================================================== +try: + os.environ['PYTHONPATH'] = os.environ['OLD_PYTHONPATH'] + del os.environ['OLD_PYTHONPATH'] +except KeyError: + if os.environ.get('PYTHONPATH') is not None: + del os.environ['PYTHONPATH'] diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/utils/sourcecode.py spyder-3.0.2+dfsg1/spyder/utils/sourcecode.py --- spyder-2.3.8+dfsg1/spyder/utils/sourcecode.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/utils/sourcecode.py 2016-10-25 02:05:23.000000000 +0200 @@ -1,14 +1,16 @@ # -*- coding: utf-8 -*- # -# Copyright © 2009-2010 Pierre Raybaut +# Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) +# (see spyder/__init__.py for details) """ Source code text utilities """ + import re + # Order is important: EOL_CHARS = (("\r\n", 'nt'), ("\n", 'posix'), ("\r", 'mac')) @@ -19,21 +21,12 @@ 'Fortran77': ('f', 'for', 'f77'), 'Fortran': ('f90', 'f95', 'f2k'), 'Idl': ('pro',), - 'Matlab': ('m',), - 'Julia': ('jl',), 'Diff': ('diff', 'patch', 'rej'), 'GetText': ('po', 'pot'), 'Nsis': ('nsi', 'nsh'), 'Html': ('htm', 'html'), - 'Css': ('css',), - 'Xml': ('xml',), - 'Js': ('js',), - 'Json': ('json', 'ipynb'), 'Cpp': ('c', 'cc', 'cpp', 'cxx', 'h', 'hh', 'hpp', 'hxx'), 'OpenCL': ('cl',), - 'Batch': ('bat', 'cmd', 'nt'), - 'Ini': ('properties', 'session', 'ini', 'inf', 'reg', 'url', - 'cfg', 'cnf', 'aut', 'iss'), 'Yaml':('yaml','yml'), } @@ -41,24 +34,28 @@ CELL_LANGUAGES = {'Python': ('#%%', '# %%', '# ', '# In[')} + def get_eol_chars(text): """Get text EOL characters""" for eol_chars, _os_name in EOL_CHARS: if text.find(eol_chars) > -1: return eol_chars + def get_os_name_from_eol_chars(eol_chars): """Return OS name from EOL characters""" for chars, os_name in EOL_CHARS: if eol_chars == chars: return os_name + def get_eol_chars_from_os_name(os_name): """Return EOL characters from OS name""" for eol_chars, name in EOL_CHARS: if name == os_name: return eol_chars + def has_mixed_eol_chars(text): """Detect if text has mixed EOL characters""" eol_chars = get_eol_chars(text) @@ -67,6 +64,7 @@ correct_text = eol_chars.join((text+eol_chars).splitlines()) return repr(correct_text) != repr(text) + def fix_indentation(text): """Replace tabs by spaces""" return text.replace('\t', ' '*4) @@ -74,7 +72,7 @@ def is_builtin(text): """Test if passed string is the name of a Python builtin object""" - from spyderlib.py3compat import builtins + from spyder.py3compat import builtins return text in [str(name) for name in dir(builtins) if not name.startswith('_')] @@ -126,8 +124,8 @@ if __name__ == '__main__': code = 'import functools\nfunctools.partial' assert get_primary_at(code, len(code)) == 'functools.partial' - assert get_identifiers(code) == ['import', 'functools', - 'functools.partial'] + assert set(get_identifiers(code)) == set(['import', 'functools', + 'functools.partial']) assert split_source(code) == ['import functools', 'functools.partial'] code = code.replace('\n', '\r\n') assert split_source(code) == ['import functools', 'functools.partial'] diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/utils/stringmatching.py spyder-3.0.2+dfsg1/spyder/utils/stringmatching.py --- spyder-2.3.8+dfsg1/spyder/utils/stringmatching.py 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/utils/stringmatching.py 2016-10-25 02:05:23.000000000 +0200 @@ -0,0 +1,280 @@ +# -*- coding: utf-8 -*- +# +# Copyright © Spyder Project Contributors +# Licensed under the terms of the MIT License +# (see spyder/__init__.py for details) + +""" +String search and match utilities usefull when filtering a list of texts. +""" + +import re + + +NOT_FOUND_SCORE = -1 +NO_SCORE = 0 + + +def get_search_regex(query, ignore_case=True): + """Returns a compiled regex pattern to search for query letters in order. + + Parameters + ---------- + query : str + String to search in another string (in order of character occurrence). + ignore_case : True + Optional value perform a case insensitive search (True by default). + + Returns + ------- + pattern : SRE_Pattern + + Notes + ----- + This function adds '.*' between the query characters and compiles the + resulting regular expression. + """ + regex_text = [char for char in query if char != ' '] + regex_text = '.*'.join(regex_text) + + regex = '({0})'.format(regex_text) + + if ignore_case: + pattern = re.compile(regex, re.IGNORECASE) + else: + pattern = re.compile(regex) + + return pattern + + +def get_search_score(query, choice, ignore_case=True, apply_regex=True, + template='{}'): + """Returns a tuple with the enriched text (if a template is provided) and + a score for the match. + + Parameters + ---------- + query : str + String with letters to search in choice (in order of appearance). + choice : str + Sentence/words in which to search for the 'query' letters. + ignore_case : bool, optional + Optional value perform a case insensitive search (True by default). + apply_regex : bool, optional + Optional value (True by default) to perform a regex search. Useful + when this function is called directly. + template : str, optional + Optional template string to surround letters found in choices. This is + useful when using a rich text editor ('{}' by default). + Examples: '{}', '{}', '{}' + + Returns + ------- + results : tuple + Tuples where the first item is the text (enriched if a template was + used) and the second item is a search score. + + Notes + ----- + The score is given according the following precedence (high to low): + + - Letters in one word and no spaces with exact match. + Example: 'up' in 'up stroke' + - Letters in one word and no spaces with partial match. + Example: 'up' in 'upstream stroke' + - Letters in one word but with skip letters. + Example: 'cls' in 'close up' + - Letters in two or more words + Example: 'cls' in 'car lost' + """ + original_choice = choice + result = (original_choice, NOT_FOUND_SCORE) + + # Handle empty string case + if not query: + return result + + if ignore_case: + query = query.lower() + choice = choice.lower() + + if apply_regex: + pattern = get_search_regex(query, ignore_case=ignore_case) + r = re.search(pattern, choice) + if r is None: + return result + else: + sep = u'-' # Matches will be replaced by this character + let = u'x' # Nonmatches (except spaed) will be replaced by this + score = 0 + + exact_words = [query == word for word in choice.split(u' ')] + partial_words = [query in word for word in choice.split(u' ')] + + if any(exact_words) or any(partial_words): + pos_start = choice.find(query) + pos_end = pos_start + len(query) + score += pos_start + text = choice.replace(query, sep*len(query), 1) + + enriched_text = original_choice[:pos_start] +\ + template.format(original_choice[pos_start:pos_end]) +\ + original_choice[pos_end:] + + if any(exact_words): + # Check if the query words exists in a word with exact match + score += 1 + elif any(partial_words): + # Check if the query words exists in a word with partial match + score += 100 + else: + # Check letter by letter + text = [l for l in original_choice] + if ignore_case: + temp_text = [l.lower() for l in original_choice] + else: + temp_text = text[:] + + # Give points to start of string + score += temp_text.index(query[0]) + + # Find the query letters and replace them by `sep`, also apply + # template as needed for enricching the letters in the text + enriched_text = text[:] + for char in query: + if char != u'' and char in temp_text: + index = temp_text.index(char) + enriched_text[index] = template.format(text[index]) + text[index] = sep + temp_text = [u' ']*(index + 1) + temp_text[index+1:] + + enriched_text = u''.join(enriched_text) + + patterns_text = [] + for i, char in enumerate(text): + if char != u' ' and char != sep: + new_char = let + else: + new_char = char + patterns_text.append(new_char) + patterns_text = u''.join(patterns_text) + for i in reversed(range(1, len(query) + 1)): + score += (len(query) - patterns_text.count(sep*i))*100000 + + temp = patterns_text.split(sep) + while u'' in temp: + temp.remove(u'') + if not patterns_text.startswith(sep): + temp = temp[1:] + if not patterns_text.endswith(sep): + temp = temp[:-1] + + for pat in temp: + score += pat.count(u' ')*10000 + score += pat.count(let)*100 + + return original_choice, enriched_text, score + + +def get_search_scores(query, choices, ignore_case=True, template='{}', + valid_only=False, sort=False): + """Search for query inside choices and return a list of tuples. + + Returns a list of tuples of text with the enriched text (if a template is + provided) and a score for the match. Lower scores imply a better match. + + Parameters + ---------- + query : str + String with letters to search in each choice (in order of appearance). + choices : list of str + List of sentences/words in which to search for the 'query' letters. + ignore_case : bool, optional + Optional value perform a case insensitive search (True by default). + template : str, optional + Optional template string to surround letters found in choices. This is + useful when using a rich text editor ('{}' by default). + Examples: '{}', '{}', '{}' + + Returns + ------- + results : list of tuples + List of tuples where the first item is the text (enriched if a + template was used) and a search score. Lower scores means better match. + """ + # First remove spaces from query + query = query.replace(' ', '') + pattern = get_search_regex(query, ignore_case) + results = [] + + for choice in choices: + r = re.search(pattern, choice) + if query and r: + result = get_search_score(query, choice, ignore_case=ignore_case, + apply_regex=False, template=template) + else: + if query: + result = (choice, choice, NOT_FOUND_SCORE) + else: + result = (choice, choice, NO_SCORE) + + if valid_only: + if result[-1] != NOT_FOUND_SCORE: + results.append(result) + else: + results.append(result) + + if sort: + results = sorted(results, key=lambda row: row[-1]) + + return results + + +def test(): + template = '{0}' + names = ['close pane', 'debug continue', 'debug exit', 'debug step into', + 'debug step over', 'debug step return', 'fullscreen mode', + 'layout preferences', 'lock unlock panes', 'maximize pane', + 'preferences', 'quit', 'restart', 'save current layout', + 'switch to breakpoints', 'switch to console', 'switch to editor', + 'switch to explorer', 'switch to find_in_files', + 'switch to historylog', 'switch to help', + 'switch to ipython_console', 'switch to onlinehelp', + 'switch to outline_explorer', 'switch to project_explorer', + 'switch to variable_explorer', + 'use next layout', 'use previous layout', 'clear line', + 'clear shell', 'inspect current object', 'blockcomment', + 'breakpoint', 'close all', 'code completion', + 'conditional breakpoint', 'configure', 'copy', 'copy line', 'cut', + 'debug', 'debug with winpdb', 'delete', 'delete line', + 'duplicate line', 'end of document', 'end of line', + 'file list management', 'find next', 'find previous', 'find text', + 'go to definition', 'go to line', 'go to next file', + 'go to previous file', 'inspect current object', 'kill next word', + 'kill previous word', 'kill to line end', 'kill to line start', + 'last edit location', 'move line down', 'move line up', + 'new file', 'next char', 'next cursor position', 'next line', + 'next word', 'open file', 'paste', 'previous char', + 'previous cursor position', 'previous line', 'previous word', + 'print', 're-run last script', 'redo', 'replace text', + 'rotate kill ring', 'run', 'run selection', 'save all', 'save as', + 'save file', 'select all', 'show/hide outline', + 'show/hide project explorer', 'start of document', + 'start of line', 'toggle comment', 'unblockcomment', 'undo', + 'yank', 'run profiler', 'run analysis'] + + a = get_search_scores('lay', names, template=template, ) + b = get_search_scores('lay', names, template=template, valid_only=True, + sort=True) + # Full results + for r in a: + print(r) + + # Ordered and filtered results + print('\n') + + for r in b: + print(r) + +if __name__ == '__main__': + test() diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/utils/syntaxhighlighters.py spyder-3.0.2+dfsg1/spyder/utils/syntaxhighlighters.py --- spyder-2.3.8+dfsg1/spyder/utils/syntaxhighlighters.py 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/utils/syntaxhighlighters.py 2016-11-16 17:12:04.000000000 +0100 @@ -0,0 +1,998 @@ +# -*- coding: utf-8 -*- +# +# Copyright © Spyder Project Contributors +# Licensed under the terms of the MIT License +# (see spyder/__init__.py for details) + +""" +Editor widget syntax highlighters based on QtGui.QSyntaxHighlighter +(Python syntax highlighting rules are inspired from idlelib) +""" + +# Standard library imports +from __future__ import print_function +import keyword +import os +import re + +# Third party imports +from qtpy.QtCore import Qt +from qtpy.QtGui import (QColor, QCursor, QFont, QSyntaxHighlighter, + QTextCharFormat, QTextOption) +from qtpy.QtWidgets import QApplication + +# Local imports +from spyder import dependencies +from spyder.config.base import _ +from spyder.config.main import CONF +from spyder.py3compat import builtins, is_text_string, to_text_string +from spyder.utils.sourcecode import CELL_LANGUAGES + + +PYGMENTS_REQVER = '>=2.0' +dependencies.add("pygments", _("Syntax highlighting for Matlab, Julia and " + "other file types"), + required_version=PYGMENTS_REQVER) + + +# ============================================================================= +# Constants +# ============================================================================= +COLOR_SCHEME_KEYS = { + "background": _("Background:"), + "currentline": _("Current line:"), + "currentcell": _("Current cell:"), + "occurrence": _("Occurrence:"), + "ctrlclick": _("Link:"), + "sideareas": _("Side areas:"), + "matched_p": _("Matched
    parens:"), + "unmatched_p": _("Unmatched
    parens:"), + "normal": _("Normal text:"), + "keyword": _("Keyword:"), + "builtin": _("Builtin:"), + "definition": _("Definition:"), + "comment": _("Comment:"), + "string": _("String:"), + "number": _("Number:"), + "instance": _("Instance:"), + } +COLOR_SCHEME_NAMES = CONF.get('color_schemes', 'names') +# Mapping for file extensions that use Pygments highlighting but should use +# different lexers than Pygments' autodetection suggests. Keys are file +# extensions or tuples of extensions, values are Pygments lexer names. +CUSTOM_EXTENSION_LEXER = {'.ipynb': 'json', + '.txt': 'text', + '.nt': 'bat', + '.scss': 'css', + '.m': 'matlab', + ('.properties', '.session', '.inf', '.reg', '.url', + '.cfg', '.cnf', '.aut', '.iss'): 'ini'} +# Convert custom extensions into a one-to-one mapping for easier lookup. +custom_extension_lexer_mapping = {} +for key, value in CUSTOM_EXTENSION_LEXER.items(): + # Single key is mapped unchanged. + if is_text_string(key): + custom_extension_lexer_mapping[key] = value + # Tuple of keys is iterated over and each is mapped to value. + else: + for k in key: + custom_extension_lexer_mapping[k] = value + + +#============================================================================== +# Auxiliary functions +#============================================================================== +def get_color_scheme(name): + """Get a color scheme from config using its name""" + name = name.lower() + scheme = {} + for key in COLOR_SCHEME_KEYS: + try: + scheme[key] = CONF.get('color_schemes', name+'/'+key) + except: + scheme[key] = CONF.get('color_schemes', 'spyder/'+key) + return scheme + + +#============================================================================== +# Syntax highlighting color schemes +#============================================================================== +class BaseSH(QSyntaxHighlighter): + """Base Syntax Highlighter Class""" + # Syntax highlighting rules: + PROG = None + BLANKPROG = re.compile("\s+") + # Syntax highlighting states (from one text block to another): + NORMAL = 0 + # Syntax highlighting parameters. + BLANK_ALPHA_FACTOR = 0.31 + + def __init__(self, parent, font=None, color_scheme='Spyder'): + QSyntaxHighlighter.__init__(self, parent) + + self.outlineexplorer_data = {} + + self.font = font + if is_text_string(color_scheme): + self.color_scheme = get_color_scheme(color_scheme) + else: + self.color_scheme = color_scheme + + self.background_color = None + self.currentline_color = None + self.currentcell_color = None + self.occurrence_color = None + self.ctrlclick_color = None + self.sideareas_color = None + self.matched_p_color = None + self.unmatched_p_color = None + + self.formats = None + self.setup_formats(font) + + self.cell_separators = None + + def get_background_color(self): + return QColor(self.background_color) + + def get_foreground_color(self): + """Return foreground ('normal' text) color""" + return self.formats["normal"].foreground().color() + + def get_currentline_color(self): + return QColor(self.currentline_color) + + def get_currentcell_color(self): + return QColor(self.currentcell_color) + + def get_occurrence_color(self): + return QColor(self.occurrence_color) + + def get_ctrlclick_color(self): + return QColor(self.ctrlclick_color) + + def get_sideareas_color(self): + return QColor(self.sideareas_color) + + def get_matched_p_color(self): + return QColor(self.matched_p_color) + + def get_unmatched_p_color(self): + return QColor(self.unmatched_p_color) + + def get_comment_color(self): + """ Return color for the comments """ + return self.formats['comment'].foreground().color() + + def get_color_name(self, fmt): + """Return color name assigned to a given format""" + return self.formats[fmt].foreground().color().name() + + def setup_formats(self, font=None): + base_format = QTextCharFormat() + if font is not None: + self.font = font + if self.font is not None: + base_format.setFont(self.font) + self.formats = {} + colors = self.color_scheme.copy() + self.background_color = colors.pop("background") + self.currentline_color = colors.pop("currentline") + self.currentcell_color = colors.pop("currentcell") + self.occurrence_color = colors.pop("occurrence") + self.ctrlclick_color = colors.pop("ctrlclick") + self.sideareas_color = colors.pop("sideareas") + self.matched_p_color = colors.pop("matched_p") + self.unmatched_p_color = colors.pop("unmatched_p") + for name, (color, bold, italic) in list(colors.items()): + format = QTextCharFormat(base_format) + format.setForeground(QColor(color)) + format.setBackground(QColor(self.background_color)) + if bold: + format.setFontWeight(QFont.Bold) + format.setFontItalic(italic) + self.formats[name] = format + + def set_color_scheme(self, color_scheme): + if is_text_string(color_scheme): + self.color_scheme = get_color_scheme(color_scheme) + else: + self.color_scheme = color_scheme + self.setup_formats() + self.rehighlight() + + def highlightBlock(self, text): + raise NotImplementedError + + def highlight_spaces(self, text, offset=0): + """ + Make blank space less apparent by setting the foreground alpha. + This only has an effect when 'Show blank space' is turned on. + Derived classes could call this function at the end of + highlightBlock(). + """ + flags_text = self.document().defaultTextOption().flags() + show_blanks = flags_text & QTextOption.ShowTabsAndSpaces + if show_blanks: + format_leading = self.formats.get("leading", None) + format_trailing = self.formats.get("trailing", None) + match = self.BLANKPROG.search(text, offset) + while match: + start, end = match.span() + start = max([0, start+offset]) + end = max([0, end+offset]) + # Format trailing spaces at the end of the line. + if end == len(text) and format_trailing is not None: + self.setFormat(start, end, format_trailing) + # Format leading spaces, e.g. indentation. + if start == 0 and format_leading is not None: + self.setFormat(start, end, format_leading) + format = self.format(start) + color_foreground = format.foreground().color() + alpha_new = self.BLANK_ALPHA_FACTOR * color_foreground.alphaF() + color_foreground.setAlphaF(alpha_new) + self.setFormat(start, end-start, color_foreground) + match = self.BLANKPROG.search(text, match.end()) + + def get_outlineexplorer_data(self): + return self.outlineexplorer_data + + def rehighlight(self): + self.outlineexplorer_data = {} + QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) + QSyntaxHighlighter.rehighlight(self) + QApplication.restoreOverrideCursor() + + +class TextSH(BaseSH): + """Simple Text Syntax Highlighter Class (only highlight spaces)""" + def highlightBlock(self, text): + self.highlight_spaces(text) + + +class GenericSH(BaseSH): + """Generic Syntax Highlighter""" + # Syntax highlighting rules: + PROG = None # to be redefined in child classes + def highlightBlock(self, text): + text = to_text_string(text) + self.setFormat(0, len(text), self.formats["normal"]) + + match = self.PROG.search(text) + index = 0 + while match: + for key, value in list(match.groupdict().items()): + if value: + start, end = match.span(key) + index += end-start + self.setFormat(start, end-start, self.formats[key]) + + match = self.PROG.search(text, match.end()) + + self.highlight_spaces(text) + + +#============================================================================== +# Python syntax highlighter +#============================================================================== +def any(name, alternates): + "Return a named group pattern matching list of alternates." + return "(?P<%s>" % name + "|".join(alternates) + ")" + +def make_python_patterns(additional_keywords=[], additional_builtins=[]): + "Strongly inspired from idlelib.ColorDelegator.make_pat" + kwlist = keyword.kwlist + additional_keywords + builtinlist = [str(name) for name in dir(builtins) + if not name.startswith('_')] + additional_builtins + repeated = set(kwlist) & set(builtinlist) + for repeated_element in repeated: + kwlist.remove(repeated_element) + kw = r"\b" + any("keyword", kwlist) + r"\b" + builtin = r"([^.'\"\\#]\b|^)" + any("builtin", builtinlist) + r"\b" + comment = any("comment", [r"#[^\n]*"]) + instance = any("instance", [r"\bself\b"]) + number = any("number", + [r"\b[+-]?[0-9]+[lLjJ]?\b", + r"\b[+-]?0[xX][0-9A-Fa-f]+[lL]?\b", + r"\b[+-]?0[oO][0-7]+[lL]?\b", + r"\b[+-]?0[bB][01]+[lL]?\b", + r"\b[+-]?[0-9]+(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?[jJ]?\b"]) + sqstring = r"(\b[rRuU])?'[^'\\\n]*(\\.[^'\\\n]*)*'?" + dqstring = r'(\b[rRuU])?"[^"\\\n]*(\\.[^"\\\n]*)*"?' + uf_sqstring = r"(\b[rRuU])?'[^'\\\n]*(\\.[^'\\\n]*)*(\\)$(?!')$" + uf_dqstring = r'(\b[rRuU])?"[^"\\\n]*(\\.[^"\\\n]*)*(\\)$(?!")$' + sq3string = r"(\b[rRuU])?'''[^'\\]*((\\.|'(?!''))[^'\\]*)*(''')?" + dq3string = r'(\b[rRuU])?"""[^"\\]*((\\.|"(?!""))[^"\\]*)*(""")?' + uf_sq3string = r"(\b[rRuU])?'''[^'\\]*((\\.|'(?!''))[^'\\]*)*(\\)?(?!''')$" + uf_dq3string = r'(\b[rRuU])?"""[^"\\]*((\\.|"(?!""))[^"\\]*)*(\\)?(?!""")$' + string = any("string", [sq3string, dq3string, sqstring, dqstring]) + ufstring1 = any("uf_sqstring", [uf_sqstring]) + ufstring2 = any("uf_dqstring", [uf_dqstring]) + ufstring3 = any("uf_sq3string", [uf_sq3string]) + ufstring4 = any("uf_dq3string", [uf_dq3string]) + return "|".join([instance, kw, builtin, comment, + ufstring1, ufstring2, ufstring3, ufstring4, string, + number, any("SYNC", [r"\n"])]) + +class OutlineExplorerData(object): + CLASS, FUNCTION, STATEMENT, COMMENT, CELL = list(range(5)) + FUNCTION_TOKEN = 'def' + CLASS_TOKEN = 'class' + + def __init__(self): + self.text = None + self.fold_level = None + self.def_type = None + self.def_name = None + + def is_not_class_nor_function(self): + return self.def_type not in (self.CLASS, self.FUNCTION) + + def is_class_or_function(self): + return self.def_type in (self.CLASS, self.FUNCTION) + + def is_comment(self): + return self.def_type in (self.COMMENT, self.CELL) + + def get_class_name(self): + if self.def_type == self.CLASS: + return self.def_name + + def get_function_name(self): + if self.def_type == self.FUNCTION: + return self.def_name + + def get_token(self): + if self.def_type == self.FUNCTION: + token = self.FUNCTION_TOKEN + elif self.def_type == self.CLASS: + token = self.CLASS_TOKEN + + return token + + +class PythonSH(BaseSH): + """Python Syntax Highlighter""" + # Syntax highlighting rules: + PROG = re.compile(make_python_patterns(), re.S) + IDPROG = re.compile(r"\s+(\w+)", re.S) + ASPROG = re.compile(r".*?\b(as)\b") + # Syntax highlighting states (from one text block to another): + (NORMAL, INSIDE_SQ3STRING, INSIDE_DQ3STRING, + INSIDE_SQSTRING, INSIDE_DQSTRING) = list(range(5)) + DEF_TYPES = {"def": OutlineExplorerData.FUNCTION, + "class": OutlineExplorerData.CLASS} + # Comments suitable for Outline Explorer + OECOMMENT = re.compile('^(# ?--[-]+|##[#]+ )[ -]*[^- ]+') + + def __init__(self, parent, font=None, color_scheme='Spyder'): + BaseSH.__init__(self, parent, font, color_scheme) + self.import_statements = {} + self.found_cell_separators = False + self.cell_separators = CELL_LANGUAGES['Python'] + + def highlightBlock(self, text): + text = to_text_string(text) + prev_state = self.previousBlockState() + if prev_state == self.INSIDE_DQ3STRING: + offset = -4 + text = r'""" '+text + elif prev_state == self.INSIDE_SQ3STRING: + offset = -4 + text = r"''' "+text + elif prev_state == self.INSIDE_DQSTRING: + offset = -2 + text = r'" '+text + elif prev_state == self.INSIDE_SQSTRING: + offset = -2 + text = r"' "+text + else: + offset = 0 + prev_state = self.NORMAL + + oedata = None + import_stmt = None + + self.setFormat(0, len(text), self.formats["normal"]) + + state = self.NORMAL + match = self.PROG.search(text) + while match: + for key, value in list(match.groupdict().items()): + if value: + start, end = match.span(key) + start = max([0, start+offset]) + end = max([0, end+offset]) + if key == "uf_sq3string": + self.setFormat(start, end-start, + self.formats["string"]) + state = self.INSIDE_SQ3STRING + elif key == "uf_dq3string": + self.setFormat(start, end-start, + self.formats["string"]) + state = self.INSIDE_DQ3STRING + elif key == "uf_sqstring": + self.setFormat(start, end-start, + self.formats["string"]) + state = self.INSIDE_SQSTRING + elif key == "uf_dqstring": + self.setFormat(start, end-start, + self.formats["string"]) + state = self.INSIDE_DQSTRING + else: + self.setFormat(start, end-start, self.formats[key]) + if key == "comment": + if text.lstrip().startswith(self.cell_separators): + self.found_cell_separators = True + oedata = OutlineExplorerData() + oedata.text = to_text_string(text).strip() + oedata.fold_level = start + oedata.def_type = OutlineExplorerData.CELL + oedata.def_name = text.strip() + elif self.OECOMMENT.match(text.lstrip()): + oedata = OutlineExplorerData() + oedata.text = to_text_string(text).strip() + oedata.fold_level = start + oedata.def_type = OutlineExplorerData.COMMENT + oedata.def_name = text.strip() + elif key == "keyword": + if value in ("def", "class"): + match1 = self.IDPROG.match(text, end) + if match1: + start1, end1 = match1.span(1) + self.setFormat(start1, end1-start1, + self.formats["definition"]) + oedata = OutlineExplorerData() + oedata.text = to_text_string(text) + oedata.fold_level = start + oedata.def_type = self.DEF_TYPES[ + to_text_string(value)] + oedata.def_name = text[start1:end1] + oedata.color = self.formats["definition"] + elif value in ("elif", "else", "except", "finally", + "for", "if", "try", "while", + "with"): + if text.lstrip().startswith(value): + oedata = OutlineExplorerData() + oedata.text = to_text_string(text).strip() + oedata.fold_level = start + oedata.def_type = \ + OutlineExplorerData.STATEMENT + oedata.def_name = text.strip() + elif value == "import": + import_stmt = text.strip() + # color all the "as" words on same line, except + # if in a comment; cheap approximation to the + # truth + if '#' in text: + endpos = text.index('#') + else: + endpos = len(text) + while True: + match1 = self.ASPROG.match(text, end, + endpos) + if not match1: + break + start, end = match1.span(1) + self.setFormat(start, end-start, + self.formats["keyword"]) + + match = self.PROG.search(text, match.end()) + + self.setCurrentBlockState(state) + + # Use normal format for indentation and trailing spaces. + self.formats['leading'] = self.formats['normal'] + self.formats['trailing'] = self.formats['normal'] + self.highlight_spaces(text, offset) + + if oedata is not None: + block_nb = self.currentBlock().blockNumber() + self.outlineexplorer_data[block_nb] = oedata + self.outlineexplorer_data['found_cell_separators'] = self.found_cell_separators + if import_stmt is not None: + block_nb = self.currentBlock().blockNumber() + self.import_statements[block_nb] = import_stmt + + def get_import_statements(self): + return list(self.import_statements.values()) + + def rehighlight(self): + self.import_statements = {} + self.found_cell_separators = False + BaseSH.rehighlight(self) + + +#============================================================================== +# Cython syntax highlighter +#============================================================================== +C_TYPES = 'bool char double enum float int long mutable short signed struct unsigned void' + +class CythonSH(PythonSH): + """Cython Syntax Highlighter""" + ADDITIONAL_KEYWORDS = ["cdef", "ctypedef", "cpdef", "inline", "cimport", + "DEF"] + ADDITIONAL_BUILTINS = C_TYPES.split() + PROG = re.compile(make_python_patterns(ADDITIONAL_KEYWORDS, + ADDITIONAL_BUILTINS), re.S) + IDPROG = re.compile(r"\s+([\w\.]+)", re.S) + + +#============================================================================== +# Enaml syntax highlighter +#============================================================================== +class EnamlSH(PythonSH): + """Enaml Syntax Highlighter""" + ADDITIONAL_KEYWORDS = ["enamldef", "template", "attr", "event", "const", "alias", + "func"] + ADDITIONAL_BUILTINS = [] + PROG = re.compile(make_python_patterns(ADDITIONAL_KEYWORDS, + ADDITIONAL_BUILTINS), re.S) + IDPROG = re.compile(r"\s+([\w\.]+)", re.S) + + +#============================================================================== +# C/C++ syntax highlighter +#============================================================================== +C_KEYWORDS1 = 'and and_eq bitand bitor break case catch const const_cast continue default delete do dynamic_cast else explicit export extern for friend goto if inline namespace new not not_eq operator or or_eq private protected public register reinterpret_cast return sizeof static static_cast switch template throw try typedef typeid typename union using virtual while xor xor_eq' +C_KEYWORDS2 = 'a addindex addtogroup anchor arg attention author b brief bug c class code date def defgroup deprecated dontinclude e em endcode endhtmlonly ifdef endif endlatexonly endlink endverbatim enum example exception f$ file fn hideinitializer htmlinclude htmlonly if image include ingroup internal invariant interface latexonly li line link mainpage name namespace nosubgrouping note overload p page par param post pre ref relates remarks return retval sa section see showinitializer since skip skipline subsection test throw todo typedef union until var verbatim verbinclude version warning weakgroup' +C_KEYWORDS3 = 'asm auto class compl false true volatile wchar_t' + +def make_generic_c_patterns(keywords, builtins, + instance=None, define=None, comment=None): + "Strongly inspired from idlelib.ColorDelegator.make_pat" + kw = r"\b" + any("keyword", keywords.split()) + r"\b" + builtin = r"\b" + any("builtin", builtins.split()+C_TYPES.split()) + r"\b" + if comment is None: + comment = any("comment", [r"//[^\n]*", r"\/\*(.*?)\*\/"]) + comment_start = any("comment_start", [r"\/\*"]) + comment_end = any("comment_end", [r"\*\/"]) + if instance is None: + instance = any("instance", [r"\bthis\b"]) + number = any("number", + [r"\b[+-]?[0-9]+[lL]?\b", + r"\b[+-]?0[xX][0-9A-Fa-f]+[lL]?\b", + r"\b[+-]?[0-9]+(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?\b"]) + sqstring = r"(\b[rRuU])?'[^'\\\n]*(\\.[^'\\\n]*)*'?" + dqstring = r'(\b[rRuU])?"[^"\\\n]*(\\.[^"\\\n]*)*"?' + string = any("string", [sqstring, dqstring]) + if define is None: + define = any("define", [r"#[^\n]*"]) + return "|".join([instance, kw, comment, string, number, + comment_start, comment_end, builtin, + define, any("SYNC", [r"\n"])]) + +def make_cpp_patterns(): + return make_generic_c_patterns(C_KEYWORDS1+' '+C_KEYWORDS2, C_KEYWORDS3) + +class CppSH(BaseSH): + """C/C++ Syntax Highlighter""" + # Syntax highlighting rules: + PROG = re.compile(make_cpp_patterns(), re.S) + # Syntax highlighting states (from one text block to another): + NORMAL = 0 + INSIDE_COMMENT = 1 + def __init__(self, parent, font=None, color_scheme=None): + BaseSH.__init__(self, parent, font, color_scheme) + + def highlightBlock(self, text): + text = to_text_string(text) + inside_comment = self.previousBlockState() == self.INSIDE_COMMENT + self.setFormat(0, len(text), + self.formats["comment" if inside_comment else "normal"]) + + match = self.PROG.search(text) + index = 0 + while match: + for key, value in list(match.groupdict().items()): + if value: + start, end = match.span(key) + index += end-start + if key == "comment_start": + inside_comment = True + self.setFormat(start, len(text)-start, + self.formats["comment"]) + elif key == "comment_end": + inside_comment = False + self.setFormat(start, end-start, + self.formats["comment"]) + elif inside_comment: + self.setFormat(start, end-start, + self.formats["comment"]) + elif key == "define": + self.setFormat(start, end-start, + self.formats["number"]) + else: + self.setFormat(start, end-start, self.formats[key]) + + match = self.PROG.search(text, match.end()) + + self.highlight_spaces(text) + + last_state = self.INSIDE_COMMENT if inside_comment else self.NORMAL + self.setCurrentBlockState(last_state) + + +def make_opencl_patterns(): + # Keywords: + kwstr1 = 'cl_char cl_uchar cl_short cl_ushort cl_int cl_uint cl_long cl_ulong cl_half cl_float cl_double cl_platform_id cl_device_id cl_context cl_command_queue cl_mem cl_program cl_kernel cl_event cl_sampler cl_bool cl_bitfield cl_device_type cl_platform_info cl_device_info cl_device_address_info cl_device_fp_config cl_device_mem_cache_type cl_device_local_mem_type cl_device_exec_capabilities cl_command_queue_properties cl_context_properties cl_context_info cl_command_queue_info cl_channel_order cl_channel_type cl_mem_flags cl_mem_object_type cl_mem_info cl_image_info cl_addressing_mode cl_filter_mode cl_sampler_info cl_map_flags cl_program_info cl_program_build_info cl_build_status cl_kernel_info cl_kernel_work_group_info cl_event_info cl_command_type cl_profiling_info cl_image_format' + # Constants: + kwstr2 = 'CL_FALSE, CL_TRUE, CL_PLATFORM_PROFILE, CL_PLATFORM_VERSION, CL_PLATFORM_NAME, CL_PLATFORM_VENDOR, CL_PLATFORM_EXTENSIONS, CL_DEVICE_TYPE_DEFAULT , CL_DEVICE_TYPE_CPU, CL_DEVICE_TYPE_GPU, CL_DEVICE_TYPE_ACCELERATOR, CL_DEVICE_TYPE_ALL, CL_DEVICE_TYPE, CL_DEVICE_VENDOR_ID, CL_DEVICE_MAX_COMPUTE_UNITS, CL_DEVICE_MAX_WORK_ITEM_DIMENSIONS, CL_DEVICE_MAX_WORK_GROUP_SIZE, CL_DEVICE_MAX_WORK_ITEM_SIZES, CL_DEVICE_PREFERRED_VECTOR_WIDTH_CHAR, CL_DEVICE_PREFERRED_VECTOR_WIDTH_SHORT, CL_DEVICE_PREFERRED_VECTOR_WIDTH_INT, CL_DEVICE_PREFERRED_VECTOR_WIDTH_LONG, CL_DEVICE_PREFERRED_VECTOR_WIDTH_FLOAT, CL_DEVICE_PREFERRED_VECTOR_WIDTH_DOUBLE, CL_DEVICE_MAX_CLOCK_FREQUENCY, CL_DEVICE_ADDRESS_BITS, CL_DEVICE_MAX_READ_IMAGE_ARGS, CL_DEVICE_MAX_WRITE_IMAGE_ARGS, CL_DEVICE_MAX_MEM_ALLOC_SIZE, CL_DEVICE_IMAGE2D_MAX_WIDTH, CL_DEVICE_IMAGE2D_MAX_HEIGHT, CL_DEVICE_IMAGE3D_MAX_WIDTH, CL_DEVICE_IMAGE3D_MAX_HEIGHT, CL_DEVICE_IMAGE3D_MAX_DEPTH, CL_DEVICE_IMAGE_SUPPORT, CL_DEVICE_MAX_PARAMETER_SIZE, CL_DEVICE_MAX_SAMPLERS, CL_DEVICE_MEM_BASE_ADDR_ALIGN, CL_DEVICE_MIN_DATA_TYPE_ALIGN_SIZE, CL_DEVICE_SINGLE_FP_CONFIG, CL_DEVICE_GLOBAL_MEM_CACHE_TYPE, CL_DEVICE_GLOBAL_MEM_CACHELINE_SIZE, CL_DEVICE_GLOBAL_MEM_CACHE_SIZE, CL_DEVICE_GLOBAL_MEM_SIZE, CL_DEVICE_MAX_CONSTANT_BUFFER_SIZE, CL_DEVICE_MAX_CONSTANT_ARGS, CL_DEVICE_LOCAL_MEM_TYPE, CL_DEVICE_LOCAL_MEM_SIZE, CL_DEVICE_ERROR_CORRECTION_SUPPORT, CL_DEVICE_PROFILING_TIMER_RESOLUTION, CL_DEVICE_ENDIAN_LITTLE, CL_DEVICE_AVAILABLE, CL_DEVICE_COMPILER_AVAILABLE, CL_DEVICE_EXECUTION_CAPABILITIES, CL_DEVICE_QUEUE_PROPERTIES, CL_DEVICE_NAME, CL_DEVICE_VENDOR, CL_DRIVER_VERSION, CL_DEVICE_PROFILE, CL_DEVICE_VERSION, CL_DEVICE_EXTENSIONS, CL_DEVICE_PLATFORM, CL_FP_DENORM, CL_FP_INF_NAN, CL_FP_ROUND_TO_NEAREST, CL_FP_ROUND_TO_ZERO, CL_FP_ROUND_TO_INF, CL_FP_FMA, CL_NONE, CL_READ_ONLY_CACHE, CL_READ_WRITE_CACHE, CL_LOCAL, CL_GLOBAL, CL_EXEC_KERNEL, CL_EXEC_NATIVE_KERNEL, CL_QUEUE_OUT_OF_ORDER_EXEC_MODE_ENABLE, CL_QUEUE_PROFILING_ENABLE, CL_CONTEXT_REFERENCE_COUNT, CL_CONTEXT_DEVICES, CL_CONTEXT_PROPERTIES, CL_CONTEXT_PLATFORM, CL_QUEUE_CONTEXT, CL_QUEUE_DEVICE, CL_QUEUE_REFERENCE_COUNT, CL_QUEUE_PROPERTIES, CL_MEM_READ_WRITE, CL_MEM_WRITE_ONLY, CL_MEM_READ_ONLY, CL_MEM_USE_HOST_PTR, CL_MEM_ALLOC_HOST_PTR, CL_MEM_COPY_HOST_PTR, CL_R, CL_A, CL_RG, CL_RA, CL_RGB, CL_RGBA, CL_BGRA, CL_ARGB, CL_INTENSITY, CL_LUMINANCE, CL_SNORM_INT8, CL_SNORM_INT16, CL_UNORM_INT8, CL_UNORM_INT16, CL_UNORM_SHORT_565, CL_UNORM_SHORT_555, CL_UNORM_INT_101010, CL_SIGNED_INT8, CL_SIGNED_INT16, CL_SIGNED_INT32, CL_UNSIGNED_INT8, CL_UNSIGNED_INT16, CL_UNSIGNED_INT32, CL_HALF_FLOAT, CL_FLOAT, CL_MEM_OBJECT_BUFFER, CL_MEM_OBJECT_IMAGE2D, CL_MEM_OBJECT_IMAGE3D, CL_MEM_TYPE, CL_MEM_FLAGS, CL_MEM_SIZECL_MEM_HOST_PTR, CL_MEM_HOST_PTR, CL_MEM_MAP_COUNT, CL_MEM_REFERENCE_COUNT, CL_MEM_CONTEXT, CL_IMAGE_FORMAT, CL_IMAGE_ELEMENT_SIZE, CL_IMAGE_ROW_PITCH, CL_IMAGE_SLICE_PITCH, CL_IMAGE_WIDTH, CL_IMAGE_HEIGHT, CL_IMAGE_DEPTH, CL_ADDRESS_NONE, CL_ADDRESS_CLAMP_TO_EDGE, CL_ADDRESS_CLAMP, CL_ADDRESS_REPEAT, CL_FILTER_NEAREST, CL_FILTER_LINEAR, CL_SAMPLER_REFERENCE_COUNT, CL_SAMPLER_CONTEXT, CL_SAMPLER_NORMALIZED_COORDS, CL_SAMPLER_ADDRESSING_MODE, CL_SAMPLER_FILTER_MODE, CL_MAP_READ, CL_MAP_WRITE, CL_PROGRAM_REFERENCE_COUNT, CL_PROGRAM_CONTEXT, CL_PROGRAM_NUM_DEVICES, CL_PROGRAM_DEVICES, CL_PROGRAM_SOURCE, CL_PROGRAM_BINARY_SIZES, CL_PROGRAM_BINARIES, CL_PROGRAM_BUILD_STATUS, CL_PROGRAM_BUILD_OPTIONS, CL_PROGRAM_BUILD_LOG, CL_BUILD_SUCCESS, CL_BUILD_NONE, CL_BUILD_ERROR, CL_BUILD_IN_PROGRESS, CL_KERNEL_FUNCTION_NAME, CL_KERNEL_NUM_ARGS, CL_KERNEL_REFERENCE_COUNT, CL_KERNEL_CONTEXT, CL_KERNEL_PROGRAM, CL_KERNEL_WORK_GROUP_SIZE, CL_KERNEL_COMPILE_WORK_GROUP_SIZE, CL_KERNEL_LOCAL_MEM_SIZE, CL_EVENT_COMMAND_QUEUE, CL_EVENT_COMMAND_TYPE, CL_EVENT_REFERENCE_COUNT, CL_EVENT_COMMAND_EXECUTION_STATUS, CL_COMMAND_NDRANGE_KERNEL, CL_COMMAND_TASK, CL_COMMAND_NATIVE_KERNEL, CL_COMMAND_READ_BUFFER, CL_COMMAND_WRITE_BUFFER, CL_COMMAND_COPY_BUFFER, CL_COMMAND_READ_IMAGE, CL_COMMAND_WRITE_IMAGE, CL_COMMAND_COPY_IMAGE, CL_COMMAND_COPY_IMAGE_TO_BUFFER, CL_COMMAND_COPY_BUFFER_TO_IMAGE, CL_COMMAND_MAP_BUFFER, CL_COMMAND_MAP_IMAGE, CL_COMMAND_UNMAP_MEM_OBJECT, CL_COMMAND_MARKER, CL_COMMAND_ACQUIRE_GL_OBJECTS, CL_COMMAND_RELEASE_GL_OBJECTS, command execution status, CL_COMPLETE, CL_RUNNING, CL_SUBMITTED, CL_QUEUED, CL_PROFILING_COMMAND_QUEUED, CL_PROFILING_COMMAND_SUBMIT, CL_PROFILING_COMMAND_START, CL_PROFILING_COMMAND_END, CL_CHAR_BIT, CL_SCHAR_MAX, CL_SCHAR_MIN, CL_CHAR_MAX, CL_CHAR_MIN, CL_UCHAR_MAX, CL_SHRT_MAX, CL_SHRT_MIN, CL_USHRT_MAX, CL_INT_MAX, CL_INT_MIN, CL_UINT_MAX, CL_LONG_MAX, CL_LONG_MIN, CL_ULONG_MAX, CL_FLT_DIG, CL_FLT_MANT_DIG, CL_FLT_MAX_10_EXP, CL_FLT_MAX_EXP, CL_FLT_MIN_10_EXP, CL_FLT_MIN_EXP, CL_FLT_RADIX, CL_FLT_MAX, CL_FLT_MIN, CL_FLT_EPSILON, CL_DBL_DIG, CL_DBL_MANT_DIG, CL_DBL_MAX_10_EXP, CL_DBL_MAX_EXP, CL_DBL_MIN_10_EXP, CL_DBL_MIN_EXP, CL_DBL_RADIX, CL_DBL_MAX, CL_DBL_MIN, CL_DBL_EPSILON, CL_SUCCESS, CL_DEVICE_NOT_FOUND, CL_DEVICE_NOT_AVAILABLE, CL_COMPILER_NOT_AVAILABLE, CL_MEM_OBJECT_ALLOCATION_FAILURE, CL_OUT_OF_RESOURCES, CL_OUT_OF_HOST_MEMORY, CL_PROFILING_INFO_NOT_AVAILABLE, CL_MEM_COPY_OVERLAP, CL_IMAGE_FORMAT_MISMATCH, CL_IMAGE_FORMAT_NOT_SUPPORTED, CL_BUILD_PROGRAM_FAILURE, CL_MAP_FAILURE, CL_INVALID_VALUE, CL_INVALID_DEVICE_TYPE, CL_INVALID_PLATFORM, CL_INVALID_DEVICE, CL_INVALID_CONTEXT, CL_INVALID_QUEUE_PROPERTIES, CL_INVALID_COMMAND_QUEUE, CL_INVALID_HOST_PTR, CL_INVALID_MEM_OBJECT, CL_INVALID_IMAGE_FORMAT_DESCRIPTOR, CL_INVALID_IMAGE_SIZE, CL_INVALID_SAMPLER, CL_INVALID_BINARY, CL_INVALID_BUILD_OPTIONS, CL_INVALID_PROGRAM, CL_INVALID_PROGRAM_EXECUTABLE, CL_INVALID_KERNEL_NAME, CL_INVALID_KERNEL_DEFINITION, CL_INVALID_KERNEL, CL_INVALID_ARG_INDEX, CL_INVALID_ARG_VALUE, CL_INVALID_ARG_SIZE, CL_INVALID_KERNEL_ARGS, CL_INVALID_WORK_DIMENSION, CL_INVALID_WORK_GROUP_SIZE, CL_INVALID_WORK_ITEM_SIZE, CL_INVALID_GLOBAL_OFFSET, CL_INVALID_EVENT_WAIT_LIST, CL_INVALID_EVENT, CL_INVALID_OPERATION, CL_INVALID_GL_OBJECT, CL_INVALID_BUFFER_SIZE, CL_INVALID_MIP_LEVEL, CL_INVALID_GLOBAL_WORK_SIZE' + # Functions: + builtins = 'clGetPlatformIDs, clGetPlatformInfo, clGetDeviceIDs, clGetDeviceInfo, clCreateContext, clCreateContextFromType, clReleaseContext, clGetContextInfo, clCreateCommandQueue, clRetainCommandQueue, clReleaseCommandQueue, clGetCommandQueueInfo, clSetCommandQueueProperty, clCreateBuffer, clCreateImage2D, clCreateImage3D, clRetainMemObject, clReleaseMemObject, clGetSupportedImageFormats, clGetMemObjectInfo, clGetImageInfo, clCreateSampler, clRetainSampler, clReleaseSampler, clGetSamplerInfo, clCreateProgramWithSource, clCreateProgramWithBinary, clRetainProgram, clReleaseProgram, clBuildProgram, clUnloadCompiler, clGetProgramInfo, clGetProgramBuildInfo, clCreateKernel, clCreateKernelsInProgram, clRetainKernel, clReleaseKernel, clSetKernelArg, clGetKernelInfo, clGetKernelWorkGroupInfo, clWaitForEvents, clGetEventInfo, clRetainEvent, clReleaseEvent, clGetEventProfilingInfo, clFlush, clFinish, clEnqueueReadBuffer, clEnqueueWriteBuffer, clEnqueueCopyBuffer, clEnqueueReadImage, clEnqueueWriteImage, clEnqueueCopyImage, clEnqueueCopyImageToBuffer, clEnqueueCopyBufferToImage, clEnqueueMapBuffer, clEnqueueMapImage, clEnqueueUnmapMemObject, clEnqueueNDRangeKernel, clEnqueueTask, clEnqueueNativeKernel, clEnqueueMarker, clEnqueueWaitForEvents, clEnqueueBarrier' + # Qualifiers: + qualifiers = '__global __local __constant __private __kernel' + keyword_list = C_KEYWORDS1+' '+C_KEYWORDS2+' '+kwstr1+' '+kwstr2 + builtin_list = C_KEYWORDS3+' '+builtins+' '+qualifiers + return make_generic_c_patterns(keyword_list, builtin_list) + +class OpenCLSH(CppSH): + """OpenCL Syntax Highlighter""" + PROG = re.compile(make_opencl_patterns(), re.S) + + +#============================================================================== +# Fortran Syntax Highlighter +#============================================================================== + +def make_fortran_patterns(): + "Strongly inspired from idlelib.ColorDelegator.make_pat" + kwstr = 'access action advance allocatable allocate apostrophe assign assignment associate asynchronous backspace bind blank blockdata call case character class close common complex contains continue cycle data deallocate decimal delim default dimension direct do dowhile double doubleprecision else elseif elsewhere encoding end endassociate endblockdata enddo endfile endforall endfunction endif endinterface endmodule endprogram endselect endsubroutine endtype endwhere entry eor equivalence err errmsg exist exit external file flush fmt forall form format formatted function go goto id if implicit in include inout integer inquire intent interface intrinsic iomsg iolength iostat kind len logical module name named namelist nextrec nml none nullify number only open opened operator optional out pad parameter pass pause pending pointer pos position precision print private program protected public quote read readwrite real rec recl recursive result return rewind save select selectcase selecttype sequential sign size stat status stop stream subroutine target then to type unformatted unit use value volatile wait where while write' + bistr1 = 'abs achar acos acosd adjustl adjustr aimag aimax0 aimin0 aint ajmax0 ajmin0 akmax0 akmin0 all allocated alog alog10 amax0 amax1 amin0 amin1 amod anint any asin asind associated atan atan2 atan2d atand bitest bitl bitlr bitrl bjtest bit_size bktest break btest cabs ccos cdabs cdcos cdexp cdlog cdsin cdsqrt ceiling cexp char clog cmplx conjg cos cosd cosh count cpu_time cshift csin csqrt dabs dacos dacosd dasin dasind datan datan2 datan2d datand date date_and_time dble dcmplx dconjg dcos dcosd dcosh dcotan ddim dexp dfloat dflotk dfloti dflotj digits dim dimag dint dlog dlog10 dmax1 dmin1 dmod dnint dot_product dprod dreal dsign dsin dsind dsinh dsqrt dtan dtand dtanh eoshift epsilon errsns exp exponent float floati floatj floatk floor fraction free huge iabs iachar iand ibclr ibits ibset ichar idate idim idint idnint ieor ifix iiabs iiand iibclr iibits iibset iidim iidint iidnnt iieor iifix iint iior iiqint iiqnnt iishft iishftc iisign ilen imax0 imax1 imin0 imin1 imod index inint inot int int1 int2 int4 int8 iqint iqnint ior ishft ishftc isign isnan izext jiand jibclr jibits jibset jidim jidint jidnnt jieor jifix jint jior jiqint jiqnnt jishft jishftc jisign jmax0 jmax1 jmin0 jmin1 jmod jnint jnot jzext kiabs kiand kibclr kibits kibset kidim kidint kidnnt kieor kifix kind kint kior kishft kishftc kisign kmax0 kmax1 kmin0 kmin1 kmod knint knot kzext lbound leadz len len_trim lenlge lge lgt lle llt log log10 logical lshift malloc matmul max max0 max1 maxexponent maxloc maxval merge min min0 min1 minexponent minloc minval mod modulo mvbits nearest nint not nworkers number_of_processors pack popcnt poppar precision present product radix random random_number random_seed range real repeat reshape rrspacing rshift scale scan secnds selected_int_kind selected_real_kind set_exponent shape sign sin sind sinh size sizeof sngl snglq spacing spread sqrt sum system_clock tan tand tanh tiny transfer transpose trim ubound unpack verify' + bistr2 = 'cdabs cdcos cdexp cdlog cdsin cdsqrt cotan cotand dcmplx dconjg dcotan dcotand decode dimag dll_export dll_import doublecomplex dreal dvchk encode find flen flush getarg getcharqq getcl getdat getenv gettim hfix ibchng identifier imag int1 int2 int4 intc intrup invalop iostat_msg isha ishc ishl jfix lacfar locking locnear map nargs nbreak ndperr ndpexc offset ovefl peekcharqq precfill prompt qabs qacos qacosd qasin qasind qatan qatand qatan2 qcmplx qconjg qcos qcosd qcosh qdim qexp qext qextd qfloat qimag qlog qlog10 qmax1 qmin1 qmod qreal qsign qsin qsind qsinh qsqrt qtan qtand qtanh ran rand randu rewrite segment setdat settim system timer undfl unlock union val virtual volatile zabs zcos zexp zlog zsin zsqrt' + kw = r"\b" + any("keyword", kwstr.split()) + r"\b" + builtin = r"\b" + any("builtin", bistr1.split()+bistr2.split()) + r"\b" + comment = any("comment", [r"\![^\n]*"]) + number = any("number", + [r"\b[+-]?[0-9]+[lL]?\b", + r"\b[+-]?0[xX][0-9A-Fa-f]+[lL]?\b", + r"\b[+-]?[0-9]+(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?\b"]) + sqstring = r"(\b[rRuU])?'[^'\\\n]*(\\.[^'\\\n]*)*'?" + dqstring = r'(\b[rRuU])?"[^"\\\n]*(\\.[^"\\\n]*)*"?' + string = any("string", [sqstring, dqstring]) + return "|".join([kw, comment, string, number, builtin, + any("SYNC", [r"\n"])]) + +class FortranSH(BaseSH): + """Fortran Syntax Highlighter""" + # Syntax highlighting rules: + PROG = re.compile(make_fortran_patterns(), re.S|re.I) + IDPROG = re.compile(r"\s+(\w+)", re.S) + # Syntax highlighting states (from one text block to another): + NORMAL = 0 + def __init__(self, parent, font=None, color_scheme=None): + BaseSH.__init__(self, parent, font, color_scheme) + + def highlightBlock(self, text): + text = to_text_string(text) + self.setFormat(0, len(text), self.formats["normal"]) + + match = self.PROG.search(text) + index = 0 + while match: + for key, value in list(match.groupdict().items()): + if value: + start, end = match.span(key) + index += end-start + self.setFormat(start, end-start, self.formats[key]) + if value.lower() in ("subroutine", "module", "function"): + match1 = self.IDPROG.match(text, end) + if match1: + start1, end1 = match1.span(1) + self.setFormat(start1, end1-start1, + self.formats["definition"]) + + match = self.PROG.search(text, match.end()) + + self.highlight_spaces(text) + +class Fortran77SH(FortranSH): + """Fortran 77 Syntax Highlighter""" + def highlightBlock(self, text): + text = to_text_string(text) + if text.startswith(("c", "C")): + self.setFormat(0, len(text), self.formats["comment"]) + self.highlight_spaces(text) + else: + FortranSH.highlightBlock(self, text) + self.setFormat(0, 5, self.formats["comment"]) + self.setFormat(73, max([73, len(text)]), + self.formats["comment"]) + + +#============================================================================== +# IDL highlighter +# +# Contribution from Stuart Mumford (Littlemumford) - 02/02/2012 +# See Issue #850 +#============================================================================== +def make_idl_patterns(): + "Strongly inspired from idlelib.ColorDelegator.make_pat" + kwstr = 'begin of pro function endfor endif endwhile endrep endcase endswitch end if then else for do while repeat until break case switch common continue exit return goto help message print read retall stop' + bistr1 = 'a_correlate abs acos adapt_hist_equal alog alog10 amoeba arg_present arra_equal array_indices ascii_template asin assoc atan beseli beselj besel k besely beta bilinear bin_date binary_template dinfgen dinomial blk_con broyden bytarr byte bytscl c_correlate call_external call_function ceil chebyshev check_math chisqr_cvf chisqr_pdf choldc cholsol cindgen clust_wts cluster color_quan colormap_applicable comfit complex complexarr complexround compute_mesh_normals cond congrid conj convert_coord convol coord2to3 correlate cos cosh cramer create_struct crossp crvlength ct_luminance cti_test curvefit cv_coord cvttobm cw_animate cw_arcball cw_bgroup cw_clr_index cw_colorsel cw_defroi cw_field cw_filesel cw_form cw_fslider cw_light_editor cw_orient cw_palette_editor cw_pdmenu cw_rgbslider cw_tmpl cw_zoom dblarr dcindgen dcomplexarr defroi deriv derivsig determ diag_matrix dialog_message dialog_pickfile pialog_printersetup dialog_printjob dialog_read_image dialog_write_image digital_filter dilate dindgen dist double eigenql eigenvec elmhes eof erode erf erfc erfcx execute exp expand_path expint extrac extract_slice f_cvf f_pdf factorial fft file_basename file_dirname file_expand_path file_info file_same file_search file_test file_which filepath findfile findgen finite fix float floor fltarr format_axis_values fstat fulstr fv_test fx_root fz_roots gamma gauss_cvf gauss_pdf gauss2dfit gaussfit gaussint get_drive_list get_kbrd get_screen_size getenv grid_tps grid3 griddata gs_iter hanning hdf_browser hdf_read hilbert hist_2d hist_equal histogram hough hqr ibeta identity idl_validname idlitsys_createtool igamma imaginary indgen int_2d int_3d int_tabulated intarr interpol interpolate invert ioctl ishft julday keword_set krig2d kurtosis kw_test l64indgen label_date label_region ladfit laguerre la_cholmprove la_cholsol la_Determ la_eigenproblem la_eigenql la_eigenvec la_elmhes la_gm_linear_model la_hqr la_invert la_least_square_equality la_least_squares la_linear_equation la_lumprove la_lusol la_trimprove la_trisol leefit legendre linbcg lindgen linfit ll_arc_distance lmfit lmgr lngamma lnp_test locale_get logical_and logical_or logical_true lon64arr lonarr long long64 lsode lu_complex lumprove lusol m_correlate machar make_array map_2points map_image map_patch map_proj_forward map_proj_init map_proj_inverse matrix_multiply matrix_power max md_test mean meanabsdev median memory mesh_clip mesh_decimate mesh_issolid mesh_merge mesh_numtriangles mesh_smooth mesh_surfacearea mesh_validate mesh_volume min min_curve_surf moment morph_close morph_distance morph_gradient morph_histormiss morph_open morph_thin morph_tophat mpeg_open msg_cat_open n_elements n_params n_tags newton norm obj_class obj_isa obj_new obj_valid objarr p_correlate path_sep pcomp pnt_line polar_surface poly poly_2d poly_area poly_fit polyfillv ployshade primes product profile profiles project_vol ptr_new ptr_valid ptrarr qgrid3 qromb qromo qsimp query_bmp query_dicom query_image query_jpeg query_mrsid query_pict query_png query_ppm query_srf query_tiff query_wav r_correlate r_test radon randomn randomu ranks read_ascii read_binary read_bmp read_dicom read_image read_mrsid read_png read_spr read_sylk read_tiff read_wav read_xwd real_part rebin recall_commands recon3 reform region_grow regress replicate reverse rk4 roberts rot rotate round routine_info rs_test s_test savgol search2d search3d sfit shift shmdebug shmvar simplex sin sindgen sinh size skewness smooth sobel sort sph_scat spher_harm spl_init spl_interp spline spline_p sprsab sprsax sprsin sprstp sqrt standardize stddev strarr strcmp strcompress stregex string strjoin strlen strlowcase strmatch strmessage strmid strpos strsplit strtrim strupcase svdfit svsol swap_endian systime t_cvf t_pdf tag_names tan tanh temporary tetra_clip tetra_surface tetra_volume thin timegen tm_test total trace transpose tri_surf trigrid trisol ts_coef ts_diff ts_fcast ts_smooth tvrd uindgen unit uintarr ul64indgen ulindgen ulon64arr ulonarr ulong ulong64 uniq value_locate variance vert_t3d voigt voxel_proj warp_tri watershed where widget_actevix widget_base widget_button widget_combobox widget_draw widget_droplist widget_event widget_info widget_label widget_list widget_propertsheet widget_slider widget_tab widget_table widget_text widget_tree write_sylk wtn xfont xregistered xsq_test' + bistr2 = 'annotate arrow axis bar_plot blas_axpy box_cursor breakpoint byteorder caldata calendar call_method call_procedure catch cd cir_3pnt close color_convert compile_opt constrained_min contour copy_lun cpu create_view cursor cw_animate_getp cw_animate_load cw_animate_run cw_light_editor_get cw_light_editor_set cw_palette_editor_get cw_palette_editor_set define_key define_msgblk define_msgblk_from_file defsysv delvar device dfpmin dissolve dlm_load doc_librar draw_roi efont empty enable_sysrtn erase errplot expand file_chmod file_copy file_delete file_lines file_link file_mkdir file_move file_readlink flick flow3 flush forward_function free_lun funct gamma_ct get_lun grid_input h_eq_ct h_eq_int heap_free heap_gc hls hsv icontour iimage image_cont image_statistics internal_volume iplot isocontour isosurface isurface itcurrent itdelete itgetcurrent itregister itreset ivolume journal la_choldc la_ludc la_svd la_tridc la_triql la_trired linkimage loadct ludc make_dll map_continents map_grid map_proj_info map_set mesh_obj mk_html_help modifyct mpeg_close mpeg_put mpeg_save msg_cat_close msg_cat_compile multi obj_destroy on_error on_ioerror online_help openr openw openu oplot oploterr particle_trace path_cache plot plot_3dbox plot_field ploterr plots point_lun polar_contour polyfill polywarp popd powell printf printd ps_show_fonts psafm pseudo ptr_free pushd qhull rdpix readf read_interfile read_jpeg read_pict read_ppm read_srf read_wave read_x11_bitmap reads readu reduce_colors register_cursor replicate_inplace resolve_all resolve_routine restore save scale3 scale3d set_plot set_shading setenv setup_keys shade_surf shade_surf_irr shade_volume shmmap show3 showfont skip_lun slicer3 slide_image socket spawn sph_4pnt streamline stretch strput struct_assign struct_hide surface surfr svdc swap_enian_inplace t3d tek_color threed time_test2 triangulate triql trired truncate_lun tv tvcrs tvlct tvscl usersym vector_field vel velovect voronoi wait wdelete wf_draw widget_control widget_displaycontextmenu window write_bmp write_image write_jpeg write_nrif write_pict write_png write_ppm write_spr write_srf write_tiff write_wav write_wave writeu wset wshow xbm_edit xdisplayfile xdxf xinteranimate xloadct xmanager xmng_tmpl xmtool xobjview xobjview_rotate xobjview_write_image xpalette xpcolo xplot3d xroi xsurface xvaredit xvolume xyouts zoom zoom_24' + kw = r"\b" + any("keyword", kwstr.split()) + r"\b" + builtin = r"\b" + any("builtin", bistr1.split()+bistr2.split()) + r"\b" + comment = any("comment", [r"\;[^\n]*"]) + number = any("number", + [r"\b[+-]?[0-9]+[lL]?\b", + r"\b[+-]?0[xX][0-9A-Fa-f]+[lL]?\b", + r"\b\.[0-9]d0|\.d0+[lL]?\b", + r"\b[+-]?[0-9]+(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?\b"]) + sqstring = r"(\b[rRuU])?'[^'\\\n]*(\\.[^'\\\n]*)*'?" + dqstring = r'(\b[rRuU])?"[^"\\\n]*(\\.[^"\\\n]*)*"?' + string = any("string", [sqstring, dqstring]) + return "|".join([kw, comment, string, number, builtin, + any("SYNC", [r"\n"])]) + +class IdlSH(GenericSH): + """IDL Syntax Highlighter""" + PROG = re.compile(make_idl_patterns(), re.S|re.I) + + +#============================================================================== +# Diff/Patch highlighter +#============================================================================== + +class DiffSH(BaseSH): + """Simple Diff/Patch Syntax Highlighter Class""" + def highlightBlock(self, text): + text = to_text_string(text) + if text.startswith("+++"): + self.setFormat(0, len(text), self.formats["keyword"]) + elif text.startswith("---"): + self.setFormat(0, len(text), self.formats["keyword"]) + elif text.startswith("+"): + self.setFormat(0, len(text), self.formats["string"]) + elif text.startswith("-"): + self.setFormat(0, len(text), self.formats["number"]) + elif text.startswith("@"): + self.setFormat(0, len(text), self.formats["builtin"]) + + self.highlight_spaces(text) + +#============================================================================== +# NSIS highlighter +#============================================================================== + +def make_nsis_patterns(): + "Strongly inspired from idlelib.ColorDelegator.make_pat" + kwstr1 = 'Abort AddBrandingImage AddSize AllowRootDirInstall AllowSkipFiles AutoCloseWindow BGFont BGGradient BrandingText BringToFront Call CallInstDLL Caption ClearErrors CompletedText ComponentText CopyFiles CRCCheck CreateDirectory CreateFont CreateShortCut Delete DeleteINISec DeleteINIStr DeleteRegKey DeleteRegValue DetailPrint DetailsButtonText DirText DirVar DirVerify EnableWindow EnumRegKey EnumRegValue Exec ExecShell ExecWait Exch ExpandEnvStrings File FileBufSize FileClose FileErrorText FileOpen FileRead FileReadByte FileSeek FileWrite FileWriteByte FindClose FindFirst FindNext FindWindow FlushINI Function FunctionEnd GetCurInstType GetCurrentAddress GetDlgItem GetDLLVersion GetDLLVersionLocal GetErrorLevel GetFileTime GetFileTimeLocal GetFullPathName GetFunctionAddress GetInstDirError GetLabelAddress GetTempFileName Goto HideWindow ChangeUI CheckBitmap Icon IfAbort IfErrors IfFileExists IfRebootFlag IfSilent InitPluginsDir InstallButtonText InstallColors InstallDir InstallDirRegKey InstProgressFlags InstType InstTypeGetText InstTypeSetText IntCmp IntCmpU IntFmt IntOp IsWindow LangString LicenseBkColor LicenseData LicenseForceSelection LicenseLangString LicenseText LoadLanguageFile LogSet LogText MessageBox MiscButtonText Name OutFile Page PageCallbacks PageEx PageExEnd Pop Push Quit ReadEnvStr ReadINIStr ReadRegDWORD ReadRegStr Reboot RegDLL Rename ReserveFile Return RMDir SearchPath Section SectionEnd SectionGetFlags SectionGetInstTypes SectionGetSize SectionGetText SectionIn SectionSetFlags SectionSetInstTypes SectionSetSize SectionSetText SendMessage SetAutoClose SetBrandingImage SetCompress SetCompressor SetCompressorDictSize SetCtlColors SetCurInstType SetDatablockOptimize SetDateSave SetDetailsPrint SetDetailsView SetErrorLevel SetErrors SetFileAttributes SetFont SetOutPath SetOverwrite SetPluginUnload SetRebootFlag SetShellVarContext SetSilent ShowInstDetails ShowUninstDetails ShowWindow SilentInstall SilentUnInstall Sleep SpaceTexts StrCmp StrCpy StrLen SubCaption SubSection SubSectionEnd UninstallButtonText UninstallCaption UninstallIcon UninstallSubCaption UninstallText UninstPage UnRegDLL Var VIAddVersionKey VIProductVersion WindowIcon WriteINIStr WriteRegBin WriteRegDWORD WriteRegExpandStr WriteRegStr WriteUninstaller XPStyle' + kwstr2 = 'all alwaysoff ARCHIVE auto both bzip2 components current custom details directory false FILE_ATTRIBUTE_ARCHIVE FILE_ATTRIBUTE_HIDDEN FILE_ATTRIBUTE_NORMAL FILE_ATTRIBUTE_OFFLINE FILE_ATTRIBUTE_READONLY FILE_ATTRIBUTE_SYSTEM FILE_ATTRIBUTE_TEMPORARY force grey HIDDEN hide IDABORT IDCANCEL IDIGNORE IDNO IDOK IDRETRY IDYES ifdiff ifnewer instfiles instfiles lastused leave left level license listonly lzma manual MB_ABORTRETRYIGNORE MB_DEFBUTTON1 MB_DEFBUTTON2 MB_DEFBUTTON3 MB_DEFBUTTON4 MB_ICONEXCLAMATION MB_ICONINFORMATION MB_ICONQUESTION MB_ICONSTOP MB_OK MB_OKCANCEL MB_RETRYCANCEL MB_RIGHT MB_SETFOREGROUND MB_TOPMOST MB_YESNO MB_YESNOCANCEL nevershow none NORMAL off OFFLINE on READONLY right RO show silent silentlog SYSTEM TEMPORARY text textonly true try uninstConfirm windows zlib' + kwstr3 = 'MUI_ABORTWARNING MUI_ABORTWARNING_CANCEL_DEFAULT MUI_ABORTWARNING_TEXT MUI_BGCOLOR MUI_COMPONENTSPAGE_CHECKBITMAP MUI_COMPONENTSPAGE_NODESC MUI_COMPONENTSPAGE_SMALLDESC MUI_COMPONENTSPAGE_TEXT_COMPLIST MUI_COMPONENTSPAGE_TEXT_DESCRIPTION_INFO MUI_COMPONENTSPAGE_TEXT_DESCRIPTION_TITLE MUI_COMPONENTSPAGE_TEXT_INSTTYPE MUI_COMPONENTSPAGE_TEXT_TOP MUI_CUSTOMFUNCTION_ABORT MUI_CUSTOMFUNCTION_GUIINIT MUI_CUSTOMFUNCTION_UNABORT MUI_CUSTOMFUNCTION_UNGUIINIT MUI_DESCRIPTION_TEXT MUI_DIRECTORYPAGE_BGCOLOR MUI_DIRECTORYPAGE_TEXT_DESTINATION MUI_DIRECTORYPAGE_TEXT_TOP MUI_DIRECTORYPAGE_VARIABLE MUI_DIRECTORYPAGE_VERIFYONLEAVE MUI_FINISHPAGE_BUTTON MUI_FINISHPAGE_CANCEL_ENABLED MUI_FINISHPAGE_LINK MUI_FINISHPAGE_LINK_COLOR MUI_FINISHPAGE_LINK_LOCATION MUI_FINISHPAGE_NOAUTOCLOSE MUI_FINISHPAGE_NOREBOOTSUPPORT MUI_FINISHPAGE_REBOOTLATER_DEFAULT MUI_FINISHPAGE_RUN MUI_FINISHPAGE_RUN_FUNCTION MUI_FINISHPAGE_RUN_NOTCHECKED MUI_FINISHPAGE_RUN_PARAMETERS MUI_FINISHPAGE_RUN_TEXT MUI_FINISHPAGE_SHOWREADME MUI_FINISHPAGE_SHOWREADME_FUNCTION MUI_FINISHPAGE_SHOWREADME_NOTCHECKED MUI_FINISHPAGE_SHOWREADME_TEXT MUI_FINISHPAGE_TEXT MUI_FINISHPAGE_TEXT_LARGE MUI_FINISHPAGE_TEXT_REBOOT MUI_FINISHPAGE_TEXT_REBOOTLATER MUI_FINISHPAGE_TEXT_REBOOTNOW MUI_FINISHPAGE_TITLE MUI_FINISHPAGE_TITLE_3LINES MUI_FUNCTION_DESCRIPTION_BEGIN MUI_FUNCTION_DESCRIPTION_END MUI_HEADER_TEXT MUI_HEADER_TRANSPARENT_TEXT MUI_HEADERIMAGE MUI_HEADERIMAGE_BITMAP MUI_HEADERIMAGE_BITMAP_NOSTRETCH MUI_HEADERIMAGE_BITMAP_RTL MUI_HEADERIMAGE_BITMAP_RTL_NOSTRETCH MUI_HEADERIMAGE_RIGHT MUI_HEADERIMAGE_UNBITMAP MUI_HEADERIMAGE_UNBITMAP_NOSTRETCH MUI_HEADERIMAGE_UNBITMAP_RTL MUI_HEADERIMAGE_UNBITMAP_RTL_NOSTRETCH MUI_HWND MUI_ICON MUI_INSTALLCOLORS MUI_INSTALLOPTIONS_DISPLAY MUI_INSTALLOPTIONS_DISPLAY_RETURN MUI_INSTALLOPTIONS_EXTRACT MUI_INSTALLOPTIONS_EXTRACT_AS MUI_INSTALLOPTIONS_INITDIALOG MUI_INSTALLOPTIONS_READ MUI_INSTALLOPTIONS_SHOW MUI_INSTALLOPTIONS_SHOW_RETURN MUI_INSTALLOPTIONS_WRITE MUI_INSTFILESPAGE_ABORTHEADER_SUBTEXT MUI_INSTFILESPAGE_ABORTHEADER_TEXT MUI_INSTFILESPAGE_COLORS MUI_INSTFILESPAGE_FINISHHEADER_SUBTEXT MUI_INSTFILESPAGE_FINISHHEADER_TEXT MUI_INSTFILESPAGE_PROGRESSBAR MUI_LANGDLL_ALLLANGUAGES MUI_LANGDLL_ALWAYSSHOW MUI_LANGDLL_DISPLAY MUI_LANGDLL_INFO MUI_LANGDLL_REGISTRY_KEY MUI_LANGDLL_REGISTRY_ROOT MUI_LANGDLL_REGISTRY_VALUENAME MUI_LANGDLL_WINDOWTITLE MUI_LANGUAGE MUI_LICENSEPAGE_BGCOLOR MUI_LICENSEPAGE_BUTTON MUI_LICENSEPAGE_CHECKBOX MUI_LICENSEPAGE_CHECKBOX_TEXT MUI_LICENSEPAGE_RADIOBUTTONS MUI_LICENSEPAGE_RADIOBUTTONS_TEXT_ACCEPT MUI_LICENSEPAGE_RADIOBUTTONS_TEXT_DECLINE MUI_LICENSEPAGE_TEXT_BOTTOM MUI_LICENSEPAGE_TEXT_TOP MUI_PAGE_COMPONENTS MUI_PAGE_CUSTOMFUNCTION_LEAVE MUI_PAGE_CUSTOMFUNCTION_PRE MUI_PAGE_CUSTOMFUNCTION_SHOW MUI_PAGE_DIRECTORY MUI_PAGE_FINISH MUI_PAGE_HEADER_SUBTEXT MUI_PAGE_HEADER_TEXT MUI_PAGE_INSTFILES MUI_PAGE_LICENSE MUI_PAGE_STARTMENU MUI_PAGE_WELCOME MUI_RESERVEFILE_INSTALLOPTIONS MUI_RESERVEFILE_LANGDLL MUI_SPECIALINI MUI_STARTMENU_GETFOLDER MUI_STARTMENU_WRITE_BEGIN MUI_STARTMENU_WRITE_END MUI_STARTMENUPAGE_BGCOLOR MUI_STARTMENUPAGE_DEFAULTFOLDER MUI_STARTMENUPAGE_NODISABLE MUI_STARTMENUPAGE_REGISTRY_KEY MUI_STARTMENUPAGE_REGISTRY_ROOT MUI_STARTMENUPAGE_REGISTRY_VALUENAME MUI_STARTMENUPAGE_TEXT_CHECKBOX MUI_STARTMENUPAGE_TEXT_TOP MUI_UI MUI_UI_COMPONENTSPAGE_NODESC MUI_UI_COMPONENTSPAGE_SMALLDESC MUI_UI_HEADERIMAGE MUI_UI_HEADERIMAGE_RIGHT MUI_UNABORTWARNING MUI_UNABORTWARNING_CANCEL_DEFAULT MUI_UNABORTWARNING_TEXT MUI_UNCONFIRMPAGE_TEXT_LOCATION MUI_UNCONFIRMPAGE_TEXT_TOP MUI_UNFINISHPAGE_NOAUTOCLOSE MUI_UNFUNCTION_DESCRIPTION_BEGIN MUI_UNFUNCTION_DESCRIPTION_END MUI_UNGETLANGUAGE MUI_UNICON MUI_UNPAGE_COMPONENTS MUI_UNPAGE_CONFIRM MUI_UNPAGE_DIRECTORY MUI_UNPAGE_FINISH MUI_UNPAGE_INSTFILES MUI_UNPAGE_LICENSE MUI_UNPAGE_WELCOME MUI_UNWELCOMEFINISHPAGE_BITMAP MUI_UNWELCOMEFINISHPAGE_BITMAP_NOSTRETCH MUI_UNWELCOMEFINISHPAGE_INI MUI_WELCOMEFINISHPAGE_BITMAP MUI_WELCOMEFINISHPAGE_BITMAP_NOSTRETCH MUI_WELCOMEFINISHPAGE_CUSTOMFUNCTION_INIT MUI_WELCOMEFINISHPAGE_INI MUI_WELCOMEPAGE_TEXT MUI_WELCOMEPAGE_TITLE MUI_WELCOMEPAGE_TITLE_3LINES' + bistr = 'addincludedir addplugindir AndIf cd define echo else endif error execute If ifdef ifmacrodef ifmacrondef ifndef include insertmacro macro macroend onGUIEnd onGUIInit onInit onInstFailed onInstSuccess onMouseOverSection onRebootFailed onSelChange onUserAbort onVerifyInstDir OrIf packhdr system undef verbose warning' + instance = any("instance", [r'\$\{.*?\}', r'\$[A-Za-z0-9\_]*']) + define = any("define", [r"\![^\n]*"]) + comment = any("comment", [r"\;[^\n]*", r"\#[^\n]*", r"\/\*(.*?)\*\/"]) + return make_generic_c_patterns(kwstr1+' '+kwstr2+' '+kwstr3, bistr, + instance=instance, define=define, + comment=comment) + +class NsisSH(CppSH): + """NSIS Syntax Highlighter""" + # Syntax highlighting rules: + PROG = re.compile(make_nsis_patterns(), re.S) + + +#============================================================================== +# gettext highlighter +#============================================================================== + +def make_gettext_patterns(): + "Strongly inspired from idlelib.ColorDelegator.make_pat" + kwstr = 'msgid msgstr' + kw = r"\b" + any("keyword", kwstr.split()) + r"\b" + fuzzy = any("builtin", [r"#,[^\n]*"]) + links = any("normal", [r"#:[^\n]*"]) + comment = any("comment", [r"#[^\n]*"]) + number = any("number", + [r"\b[+-]?[0-9]+[lL]?\b", + r"\b[+-]?0[xX][0-9A-Fa-f]+[lL]?\b", + r"\b[+-]?[0-9]+(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?\b"]) + sqstring = r"(\b[rRuU])?'[^'\\\n]*(\\.[^'\\\n]*)*'?" + dqstring = r'(\b[rRuU])?"[^"\\\n]*(\\.[^"\\\n]*)*"?' + string = any("string", [sqstring, dqstring]) + return "|".join([kw, string, number, fuzzy, links, comment, + any("SYNC", [r"\n"])]) + +class GetTextSH(GenericSH): + """gettext Syntax Highlighter""" + # Syntax highlighting rules: + PROG = re.compile(make_gettext_patterns(), re.S) + +#============================================================================== +# yaml highlighter +#============================================================================== + +def make_yaml_patterns(): + "Strongly inspired from sublime highlighter " + kw = any("keyword", [r":|>|-|\||\[|\]|[A-Za-z][\w\s\-\_ ]+(?=:)"]) + links = any("normal", [r"#:[^\n]*"]) + comment = any("comment", [r"#[^\n]*"]) + number = any("number", + [r"\b[+-]?[0-9]+[lL]?\b", + r"\b[+-]?0[xX][0-9A-Fa-f]+[lL]?\b", + r"\b[+-]?[0-9]+(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?\b"]) + sqstring = r"(\b[rRuU])?'[^'\\\n]*(\\.[^'\\\n]*)*'?" + dqstring = r'(\b[rRuU])?"[^"\\\n]*(\\.[^"\\\n]*)*"?' + string = any("string", [sqstring, dqstring]) + return "|".join([kw, string, number, links, comment, + any("SYNC", [r"\n"])]) + +class YamlSH(GenericSH): + """yaml Syntax Highlighter""" + # Syntax highlighting rules: + PROG = re.compile(make_yaml_patterns(), re.S) + + +#============================================================================== +# HTML highlighter +#============================================================================== + +class BaseWebSH(BaseSH): + """Base class for CSS and HTML syntax highlighters""" + NORMAL = 0 + COMMENT = 1 + + def __init__(self, parent, font=None, color_scheme=None): + BaseSH.__init__(self, parent, font, color_scheme) + + def highlightBlock(self, text): + text = to_text_string(text) + previous_state = self.previousBlockState() + + if previous_state == self.COMMENT: + self.setFormat(0, len(text), self.formats["comment"]) + else: + previous_state = self.NORMAL + self.setFormat(0, len(text), self.formats["normal"]) + + self.setCurrentBlockState(previous_state) + match = self.PROG.search(text) + + match_count = 0 + n_characters = len(text) + # There should never be more matches than characters in the text. + while match and match_count < n_characters: + match_dict = match.groupdict() + for key, value in list(match_dict.items()): + if value: + start, end = match.span(key) + if previous_state == self.COMMENT: + if key == "multiline_comment_end": + self.setCurrentBlockState(self.NORMAL) + self.setFormat(end, len(text), + self.formats["normal"]) + else: + self.setCurrentBlockState(self.COMMENT) + self.setFormat(0, len(text), + self.formats["comment"]) + else: + if key == "multiline_comment_start": + self.setCurrentBlockState(self.COMMENT) + self.setFormat(start, len(text), + self.formats["comment"]) + else: + self.setCurrentBlockState(self.NORMAL) + try: + self.setFormat(start, end-start, + self.formats[key]) + except KeyError: + # happens with unmatched end-of-comment; + # see issue 1462 + pass + + match = self.PROG.search(text, match.end()) + match_count += 1 + + self.highlight_spaces(text) + +def make_html_patterns(): + """Strongly inspired from idlelib.ColorDelegator.make_pat """ + tags = any("builtin", [r"<", r"[\?/]?>", r"(?<=<).*?(?=[ >])"]) + keywords = any("keyword", [r" [\w:-]*?(?==)"]) + string = any("string", [r'".*?"']) + comment = any("comment", [r""]) + multiline_comment_start = any("multiline_comment_start", [r""]) + return "|".join([comment, multiline_comment_start, + multiline_comment_end, tags, keywords, string]) + +class HtmlSH(BaseWebSH): + """HTML Syntax Highlighter""" + PROG = re.compile(make_html_patterns(), re.S) + + +#============================================================================== +# Pygments based omni-parser +#============================================================================== + +# IMPORTANT NOTE: +# -------------- +# Do not be tempted to generalize the use of PygmentsSH (that is tempting +# because it would lead to more generic and compact code, and not only in +# this very module) because this generic syntax highlighter is far slower +# than the native ones (all classes above). For example, a Python syntax +# highlighter based on PygmentsSH would be 2 to 3 times slower than the +# current native PythonSH syntax highlighter. + +class PygmentsSH(BaseSH): + """ Generic Pygments syntax highlighter """ + # Store the language name and a ref to the lexer + _lang_name = None + _lexer = None + # Syntax highlighting states (from one text block to another): + NORMAL = 0 + def __init__(self, parent, font=None, color_scheme=None): + # Warning: do not move out those import statements + # (pygments is an optional dependency) + from pygments.lexers import get_lexer_by_name + from pygments.token import (Text, Other, Keyword, Name, String, Number, + Comment, Generic, Token) + # Map Pygments tokens to Spyder tokens + self._tokmap = {Text: "normal", + Generic: "normal", + Other: "normal", + Keyword: "keyword", + Token.Operator: "normal", + Name.Builtin: "builtin", + Name: "normal", + Comment: "comment", + String: "string", + Number: "number"} + # Load Pygments' Lexer + if self._lang_name is not None: + self._lexer = get_lexer_by_name(self._lang_name) + BaseSH.__init__(self, parent, font, color_scheme) + + def get_fmt(self, typ): + """ Get the format code for this type """ + # Exact matches first + for key in self._tokmap: + if typ is key: + return self._tokmap[key] + # Partial (parent-> child) matches + for key in self._tokmap: + if typ in key.subtypes: + return self._tokmap[key] + return 'normal' + + def highlightBlock(self, text): + """ Actually highlight the block """ + text = to_text_string(text) + lextree = self._lexer.get_tokens(text) + ct = 0 + for item in lextree: + typ, val = item + key = self.get_fmt(typ) + start = ct + ct += len(val) + self.setFormat(start, ct-start, self.formats[key]) + + self.highlight_spaces(text) + +def guess_pygments_highlighter(filename): + """Factory to generate syntax highlighter for the given filename. + + If a syntax highlighter is not available for a particular file, this + function will attempt to generate one based on the lexers in Pygments. If + Pygments is not available or does not have an appropriate lexer, TextSH + will be returned instead. + + """ + try: + from pygments.lexers import get_lexer_for_filename, get_lexer_by_name + from pygments.util import ClassNotFound + except ImportError: + return TextSH + root, ext = os.path.splitext(filename) + if ext in custom_extension_lexer_mapping: + lexer = get_lexer_by_name(custom_extension_lexer_mapping[ext]) + else: + try: + lexer = get_lexer_for_filename(filename) + except ClassNotFound: + return TextSH + class GuessedPygmentsSH(PygmentsSH): + _lexer = lexer + return GuessedPygmentsSH diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/utils/system.py spyder-3.0.2+dfsg1/spyder/utils/system.py --- spyder-2.3.8+dfsg1/spyder/utils/system.py 2015-08-24 21:09:46.000000000 +0200 +++ spyder-3.0.2+dfsg1/spyder/utils/system.py 2016-10-25 02:05:23.000000000 +0200 @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- # -# Copyright © 2012 Pierre Raybaut +# Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) +# (see spyder/__init__.py for details) """Operating system utilities""" @@ -10,7 +10,7 @@ import os # Local imports -from spyderlib.utils import programs +from spyder.utils import programs def windows_memory_usage(): diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/utils/vcs.py spyder-3.0.2+dfsg1/spyder/utils/vcs.py --- spyder-2.3.8+dfsg1/spyder/utils/vcs.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/utils/vcs.py 2016-10-25 02:05:23.000000000 +0200 @@ -1,20 +1,21 @@ # -*- coding: utf-8 -*- # -# Copyright © 2011 Pierre Raybaut +# Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) +# (see spyder/__init__.py for details) """Utilities for version control systems""" from __future__ import print_function +import sys import os.path as osp import subprocess # Local imports -from spyderlib.utils import programs -from spyderlib.utils.misc import abspardir -from spyderlib.py3compat import PY3 +from spyder.utils import programs +from spyder.utils.misc import abspardir +from spyder.py3compat import PY3 SUPPORTED = [ @@ -38,7 +39,7 @@ class ActionToolNotFound(RuntimeError): """Exception to transmit information about supported tools for failed attempt to execute given action""" - + def __init__(self, vcsname, action, tools): RuntimeError.__init__(self) self.vcsname = vcsname @@ -99,10 +100,9 @@ ('eba7273c69df+', '2015+', 'default') """ try: - hg = programs.find_program('hg') - assert hg is not None and osp.isdir(osp.join(repopath, '.hg')) - output, _err = subprocess.Popen([hg, 'id', '-nib', repopath], - stdout=subprocess.PIPE).communicate() + assert osp.isdir(osp.join(repopath, '.hg')) + proc = programs.run_program('hg', ['id', '-nib', repopath]) + output, _err = proc.communicate() # output is now: ('eba7273c69df+ 2015+ default\n', None) # Split 2 times max to allow spaces in branch names. return tuple(output.decode().strip().split(None, 2)) @@ -112,23 +112,37 @@ def get_git_revision(repopath): - """Return Git revision for the repository located at repopath - Result is the latest commit hash, with None on error + """ + Return Git revision for the repository located at repopath + + Result is a tuple (latest commit hash, branch), with None values on + error """ try: git = programs.find_program('git') assert git is not None and osp.isdir(osp.join(repopath, '.git')) - commit = subprocess.Popen([git, 'rev-parse', '--short', 'HEAD'], - stdout=subprocess.PIPE, - cwd=repopath).communicate() + commit = programs.run_program(git, ['rev-parse', '--short', 'HEAD'], + cwd=repopath).communicate() + commit = commit[0].strip() if PY3: - commit = str(commit[0][:-1]) - commit = commit[2:-1] + commit = commit.decode(sys.getdefaultencoding()) + + # Branch + branches = programs.run_program(git, ['branch'], + cwd=repopath).communicate() + branches = branches[0] + if PY3: + branches = branches.decode(sys.getdefaultencoding()) + branches = branches.split('\n') + active_branch = [b for b in branches if b.startswith('*')] + if len(active_branch) != 1: + branch = None else: - commit = commit[0][:-1] - return commit + branch = active_branch[0].split(None, 1)[1] + + return commit, branch except (subprocess.CalledProcessError, AssertionError, AttributeError): - return None + return None, None if __name__ == '__main__': diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/utils/windows.py spyder-3.0.2+dfsg1/spyder/utils/windows.py --- spyder-2.3.8+dfsg1/spyder/utils/windows.py 2015-08-24 21:09:46.000000000 +0200 +++ spyder-3.0.2+dfsg1/spyder/utils/windows.py 2016-10-25 02:05:23.000000000 +0200 @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- # -# Copyright © 2012 Pierre Raybaut +# Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) +# (see spyder/__init__.py for details) """Windows-specific utilities""" @@ -39,7 +39,7 @@ def set_windows_appusermodelid(): """Make sure correct icon is used on Windows 7 taskbar""" try: - return windll.shell32.SetCurrentProcessExplicitAppUserModelID("spyderlib.Spyder") + return windll.shell32.SetCurrentProcessExplicitAppUserModelID("spyder.Spyder") except AttributeError: return "SetCurrentProcessExplicitAppUserModelID not found" diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/widgets/arraybuilder.py spyder-3.0.2+dfsg1/spyder/widgets/arraybuilder.py --- spyder-2.3.8+dfsg1/spyder/widgets/arraybuilder.py 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/widgets/arraybuilder.py 2016-10-25 02:05:23.000000000 +0200 @@ -0,0 +1,403 @@ +# -*- coding: utf-8 -*- +# +# Copyright © Spyder Project Contributors +# Licensed under the terms of the MIT License +# (see spyder/__init__.py for details) + +""" +Numpy Matrix/Array Builder Widget. +""" + +# TODO: +# -Set font based on caller? editor console? and adjust size of widget +# -Fix positioning +# -Use the same font as editor/console? + +# Standard library imports +from __future__ import division +import re + +# Third party imports +from qtpy.QtCore import QEvent, QPoint, Qt +from qtpy.QtWidgets import (QDialog, QHBoxLayout, QLineEdit, QTableWidget, + QTableWidgetItem, QToolButton, QToolTip, + QWidget) + +# Local imports +from spyder.config.base import _ +from spyder.utils import icon_manager as ima +from spyder.widgets.helperwidgets import HelperToolButton + + +# Constants +SHORTCUT_TABLE = "Ctrl+M" +SHORTCUT_INLINE = "Ctrl+Alt+M" +ELEMENT_SEPARATOR = ', ' +ROW_SEPARATOR = ';' +BRACES = '], [' +NAN_VALUES = ['nan', 'NAN', 'NaN', 'Na', 'NA', 'na'] + + +class NumpyArrayInline(QLineEdit): + def __init__(self, parent): + QLineEdit.__init__(self, parent) + self._parent = parent + + def keyPressEvent(self, event): + """ + Qt override. + """ + if event.key() in [Qt.Key_Enter, Qt.Key_Return]: + self._parent.process_text() + if self._parent.is_valid(): + self._parent.keyPressEvent(event) + else: + QLineEdit.keyPressEvent(self, event) + + # to catch the Tab key event + def event(self, event): + """ + Qt override. + + This is needed to be able to intercept the Tab key press event. + """ + if event.type() == QEvent.KeyPress: + if (event.key() == Qt.Key_Tab or event.key() == Qt.Key_Space): + text = self.text() + cursor = self.cursorPosition() + # fix to include in "undo/redo" history + if cursor != 0 and text[cursor-1] == ' ': + text = text[:cursor-1] + ROW_SEPARATOR + ' ' +\ + text[cursor:] + else: + text = text[:cursor] + ' ' + text[cursor:] + self.setCursorPosition(cursor) + self.setText(text) + self.setCursorPosition(cursor + 1) + return False + return QWidget.event(self, event) + + +class NumpyArrayTable(QTableWidget): + def __init__(self, parent): + QTableWidget.__init__(self, parent) + self._parent = parent + self.setRowCount(2) + self.setColumnCount(2) + self.reset_headers() + + # signals + self.cellChanged.connect(self.cell_changed) + + def keyPressEvent(self, event): + """ + Qt override. + """ + if event.key() in [Qt.Key_Enter, Qt.Key_Return]: + QTableWidget.keyPressEvent(self, event) + # To avoid having to enter one final tab + self.setDisabled(True) + self.setDisabled(False) + self._parent.keyPressEvent(event) + else: + QTableWidget.keyPressEvent(self, event) + + def cell_changed(self, row, col): + """ + """ + item = self.item(row, col) + value = None + + if item: + rows = self.rowCount() + cols = self.columnCount() + value = item.text() + + if value: + if row == rows - 1: + self.setRowCount(rows + 1) + if col == cols - 1: + self.setColumnCount(cols + 1) + self.reset_headers() + + def reset_headers(self): + """ + Update the column and row numbering in the headers. + """ + rows = self.rowCount() + cols = self.columnCount() + + for r in range(rows): + self.setVerticalHeaderItem(r, QTableWidgetItem(str(r))) + for c in range(cols): + self.setHorizontalHeaderItem(c, QTableWidgetItem(str(c))) + self.setColumnWidth(c, 40) + + def text(self): + """ + Return the entered array in a parseable form. + """ + text = [] + rows = self.rowCount() + cols = self.columnCount() + + # handle empty table case + if rows == 2 and cols == 2: + item = self.item(0, 0) + if item is None: + return '' + + for r in range(rows - 1): + for c in range(cols - 1): + item = self.item(r, c) + if item is not None: + value = item.text() + else: + value = '0' + + if not value.strip(): + value = '0' + + text.append(' ') + text.append(value) + text.append(ROW_SEPARATOR) + + return ''.join(text[:-1]) # to remove the final uneeded ; + + +class NumpyArrayDialog(QDialog): + def __init__(self, parent=None, inline=True, offset=0, force_float=False): + QDialog.__init__(self, parent=parent) + self._parent = parent + self._text = None + self._valid = None + self._offset = offset + + # TODO: add this as an option in the General Preferences? + self._force_float = force_float + + self._help_inline = _(""" + Numpy Array/Matrix Helper
    + Type an array in Matlab : [1 2;3 4]
    + or Spyder simplified syntax : 1 2;3 4 +

    + Hit 'Enter' for array or 'Ctrl+Enter' for matrix. +

    + Hint:
    + Use two spaces or two tabs to generate a ';'. + """) + + self._help_table = _(""" + Numpy Array/Matrix Helper
    + Enter an array in the table.
    + Use Tab to move between cells. +

    + Hit 'Enter' for array or 'Ctrl+Enter' for matrix. +

    + Hint:
    + Use two tabs at the end of a row to move to the next row. + """) + + # Widgets + self._button_warning = QToolButton() + self._button_help = HelperToolButton() + self._button_help.setIcon(ima.icon('MessageBoxInformation')) + + style = """ + QToolButton { + border: 1px solid grey; + padding:0px; + border-radius: 2px; + background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #f6f7fa, stop: 1 #dadbde); + } + """ + self._button_help.setStyleSheet(style) + + if inline: + self._button_help.setToolTip(self._help_inline) + self._text = NumpyArrayInline(self) + self._widget = self._text + else: + self._button_help.setToolTip(self._help_table) + self._table = NumpyArrayTable(self) + self._widget = self._table + + style = """ + QDialog { + margin:0px; + border: 1px solid grey; + padding:0px; + border-radius: 2px; + }""" + self.setStyleSheet(style) + + style = """ + QToolButton { + margin:1px; + border: 0px solid grey; + padding:0px; + border-radius: 0px; + }""" + self._button_warning.setStyleSheet(style) + + # widget setup + self.setWindowFlags(Qt.Window | Qt.Dialog | Qt.FramelessWindowHint) + self.setModal(True) + self.setWindowOpacity(0.90) + self._widget.setMinimumWidth(200) + + # layout + self._layout = QHBoxLayout() + self._layout.addWidget(self._widget) + self._layout.addWidget(self._button_warning, 1, Qt.AlignTop) + self._layout.addWidget(self._button_help, 1, Qt.AlignTop) + self.setLayout(self._layout) + + self._widget.setFocus() + + def keyPressEvent(self, event): + """ + Qt override. + """ + QToolTip.hideText() + ctrl = event.modifiers() & Qt.ControlModifier + + if event.key() in [Qt.Key_Enter, Qt.Key_Return]: + if ctrl: + self.process_text(array=False) + else: + self.process_text(array=True) + self.accept() + else: + QDialog.keyPressEvent(self, event) + + def event(self, event): + """ + Qt Override. + + Usefull when in line edit mode. + """ + if event.type() == QEvent.KeyPress and event.key() == Qt.Key_Tab: + return False + return QWidget.event(self, event) + + def process_text(self, array=True): + """ + Construct the text based on the entered content in the widget. + """ + if array: + prefix = 'np.array([[' + else: + prefix = 'np.matrix([[' + + suffix = ']])' + values = self._widget.text().strip() + + if values != '': + # cleans repeated spaces + exp = r'(\s*)' + ROW_SEPARATOR + r'(\s*)' + values = re.sub(exp, ROW_SEPARATOR, values) + values = re.sub("\s+", " ", values) + values = re.sub("]$", "", values) + values = re.sub("^\[", "", values) + values = re.sub(ROW_SEPARATOR + r'*$', '', values) + + # replaces spaces by commas + values = values.replace(' ', ELEMENT_SEPARATOR) + + # iterate to find number of rows and columns + new_values = [] + rows = values.split(ROW_SEPARATOR) + nrows = len(rows) + ncols = [] + for row in rows: + new_row = [] + elements = row.split(ELEMENT_SEPARATOR) + ncols.append(len(elements)) + for e in elements: + num = e + + # replaces not defined values + if num in NAN_VALUES: + num = 'np.nan' + + # Convert numbers to floating point + if self._force_float: + try: + num = str(float(e)) + except: + pass + new_row.append(num) + new_values.append(ELEMENT_SEPARATOR.join(new_row)) + new_values = ROW_SEPARATOR.join(new_values) + values = new_values + + # Check validity + if len(set(ncols)) == 1: + self._valid = True + else: + self._valid = False + + # Single rows are parsed as 1D arrays/matrices + if nrows == 1: + prefix = prefix[:-1] + suffix = suffix.replace("]])", "])") + + # Fix offset + offset = self._offset + braces = BRACES.replace(' ', '\n' + ' '*(offset + len(prefix) - 1)) + + values = values.replace(ROW_SEPARATOR, braces) + text = "{0}{1}{2}".format(prefix, values, suffix) + + self._text = text + else: + self._text = '' + self.update_warning() + + def update_warning(self): + """ + Updates the icon and tip based on the validity of the array content. + """ + widget = self._button_warning + if not self.is_valid(): + tip = _('Array dimensions not valid') + widget.setIcon(ima.icon('MessageBoxWarning')) + widget.setToolTip(tip) + QToolTip.showText(self._widget.mapToGlobal(QPoint(0, 5)), tip) + else: + self._button_warning.setToolTip('') + + def is_valid(self): + """ + Return if the current array state is valid. + """ + return self._valid + + def text(self): + """ + Return the parsed array/matrix text. + """ + return self._text + + @property + def array_widget(self): + """ + Return the array builder widget. + """ + return self._widget + + +def test(): # pragma: no cover + from spyder.utils.qthelpers import qapplication + app = qapplication() + dlg_table = NumpyArrayDialog(None, inline=False) + dlg_inline = NumpyArrayDialog(None, inline=True) + dlg_table.show() + dlg_inline.show() + app.exec_() + + +if __name__ == "__main__": # pragma: no cover + test() diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/widgets/arrayeditor.py spyder-3.0.2+dfsg1/spyder/widgets/arrayeditor.py --- spyder-2.3.8+dfsg1/spyder/widgets/arrayeditor.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/widgets/arrayeditor.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,778 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright © 2009-2012 Pierre Raybaut -# Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) - -""" -NumPy Array Editor Dialog based on Qt -""" - -# pylint: disable=C0103 -# pylint: disable=R0903 -# pylint: disable=R0911 -# pylint: disable=R0201 - -from __future__ import print_function - -from spyderlib.qt.QtGui import (QHBoxLayout, QColor, QTableView, QItemDelegate, - QLineEdit, QCheckBox, QGridLayout, QCursor, - QDoubleValidator, QDialog, QDialogButtonBox, - QMessageBox, QPushButton, QInputDialog, QMenu, - QApplication, QKeySequence, QLabel, QComboBox, - QSpinBox, QStackedWidget, QWidget, QVBoxLayout) -from spyderlib.qt.QtCore import (Qt, QModelIndex, QAbstractTableModel, SIGNAL, - SLOT) -from spyderlib.qt.compat import to_qvariant, from_qvariant - -import numpy as np - -# Local imports -from spyderlib.baseconfig import _ -from spyderlib.guiconfig import get_font, new_shortcut -from spyderlib.utils.qthelpers import (add_actions, create_action, keybinding, - qapplication, get_icon) -from spyderlib.py3compat import io, to_text_string, is_text_string - -# Note: string and unicode data types will be formatted with '%s' (see below) -SUPPORTED_FORMATS = { - 'single': '%.3f', - 'double': '%.3f', - 'float_': '%.3f', - 'longfloat': '%.3f', - 'float32': '%.3f', - 'float64': '%.3f', - 'float96': '%.3f', - 'float128': '%.3f', - 'csingle': '%r', - 'complex_': '%r', - 'clongfloat': '%r', - 'complex64': '%r', - 'complex128': '%r', - 'complex192': '%r', - 'complex256': '%r', - 'byte': '%d', - 'short': '%d', - 'intc': '%d', - 'int_': '%d', - 'longlong': '%d', - 'intp': '%d', - 'int8': '%d', - 'int16': '%d', - 'int32': '%d', - 'int64': '%d', - 'ubyte': '%d', - 'ushort': '%d', - 'uintc': '%d', - 'uint': '%d', - 'ulonglong': '%d', - 'uintp': '%d', - 'uint8': '%d', - 'uint16': '%d', - 'uint32': '%d', - 'uint64': '%d', - 'bool_': '%r', - 'bool8': '%r', - 'bool': '%r', - } - - -LARGE_SIZE = 5e5 -LARGE_NROWS = 1e5 -LARGE_COLS = 60 - - -def is_float(dtype): - """Return True if datatype dtype is a float kind""" - return ('float' in dtype.name) or dtype.name in ['single', 'double'] - -def is_number(dtype): - """Return True is datatype dtype is a number kind""" - return is_float(dtype) or ('int' in dtype.name) or ('long' in dtype.name) \ - or ('short' in dtype.name) - -def get_idx_rect(index_list): - """Extract the boundaries from a list of indexes""" - rows, cols = list(zip(*[(i.row(), i.column()) for i in index_list])) - return ( min(rows), max(rows), min(cols), max(cols) ) - - -class ArrayModel(QAbstractTableModel): - """Array Editor Table Model""" - - ROWS_TO_LOAD = 500 - COLS_TO_LOAD = 40 - - def __init__(self, data, format="%.3f", xlabels=None, ylabels=None, - readonly=False, parent=None): - QAbstractTableModel.__init__(self) - - self.dialog = parent - self.changes = {} - self.xlabels = xlabels - self.ylabels = ylabels - self.readonly = readonly - self.test_array = np.array([0], dtype=data.dtype) - - # for complex numbers, shading will be based on absolute value - # but for all other types it will be the real part - if data.dtype in (np.complex64, np.complex128): - self.color_func = np.abs - else: - self.color_func = np.real - - # Backgroundcolor settings - huerange = [.66, .99] # Hue - self.sat = .7 # Saturation - self.val = 1. # Value - self.alp = .6 # Alpha-channel - - self._data = data - self._format = format - - self.total_rows = self._data.shape[0] - self.total_cols = self._data.shape[1] - size = self.total_rows * self.total_cols - - try: - self.vmin = self.color_func(data).min() - self.vmax = self.color_func(data).max() - if self.vmax == self.vmin: - self.vmin -= 1 - self.hue0 = huerange[0] - self.dhue = huerange[1]-huerange[0] - self.bgcolor_enabled = True - except TypeError: - self.vmin = None - self.vmax = None - self.hue0 = None - self.dhue = None - self.bgcolor_enabled = False - - # Use paging when the total size, number of rows or number of - # columns is too large - if size > LARGE_SIZE: - self.rows_loaded = self.ROWS_TO_LOAD - self.cols_loaded = self.COLS_TO_LOAD - else: - if self.total_rows > LARGE_NROWS: - self.rows_loaded = self.ROWS_TO_LOAD - else: - self.rows_loaded = self.total_rows - if self.total_cols > LARGE_COLS: - self.cols_loaded = self.COLS_TO_LOAD - else: - self.cols_loaded = self.total_cols - - def get_format(self): - """Return current format""" - # Avoid accessing the private attribute _format from outside - return self._format - - def get_data(self): - """Return data""" - return self._data - - def set_format(self, format): - """Change display format""" - self._format = format - self.reset() - - def columnCount(self, qindex=QModelIndex()): - """Array column number""" - if self.total_cols <= self.cols_loaded: - return self.total_cols - else: - return self.cols_loaded - - def rowCount(self, qindex=QModelIndex()): - """Array row number""" - if self.total_rows <= self.rows_loaded: - return self.total_rows - else: - return self.rows_loaded - - def can_fetch_more(self, rows=False, columns=False): - if rows: - if self.total_rows > self.rows_loaded: - return True - else: - return False - if columns: - if self.total_cols > self.cols_loaded: - return True - else: - return False - - def fetch_more(self, rows=False, columns=False): - if self.can_fetch_more(rows=rows): - reminder = self.total_rows - self.rows_loaded - items_to_fetch = min(reminder, self.ROWS_TO_LOAD) - self.beginInsertRows(QModelIndex(), self.rows_loaded, - self.rows_loaded + items_to_fetch - 1) - self.rows_loaded += items_to_fetch - self.endInsertRows() - if self.can_fetch_more(columns=columns): - reminder = self.total_cols - self.cols_loaded - items_to_fetch = min(reminder, self.COLS_TO_LOAD) - self.beginInsertColumns(QModelIndex(), self.cols_loaded, - self.cols_loaded + items_to_fetch - 1) - self.cols_loaded += items_to_fetch - self.endInsertColumns() - - def bgcolor(self, state): - """Toggle backgroundcolor""" - self.bgcolor_enabled = state > 0 - self.reset() - - def get_value(self, index): - i = index.row() - j = index.column() - return self.changes.get((i, j), self._data[i, j]) - - def data(self, index, role=Qt.DisplayRole): - """Cell content""" - if not index.isValid(): - return to_qvariant() - value = self.get_value(index) - if role == Qt.DisplayRole: - if value is np.ma.masked: - return '' - else: - return to_qvariant(self._format % value) - elif role == Qt.TextAlignmentRole: - return to_qvariant(int(Qt.AlignCenter|Qt.AlignVCenter)) - elif role == Qt.BackgroundColorRole and self.bgcolor_enabled\ - and value is not np.ma.masked: - hue = self.hue0+\ - self.dhue*(self.vmax-self.color_func(value))\ - /(self.vmax-self.vmin) - hue = float(np.abs(hue)) - color = QColor.fromHsvF(hue, self.sat, self.val, self.alp) - return to_qvariant(color) - elif role == Qt.FontRole: - return to_qvariant(get_font('arrayeditor')) - return to_qvariant() - - def setData(self, index, value, role=Qt.EditRole): - """Cell content change""" - if not index.isValid() or self.readonly: - return False - i = index.row() - j = index.column() - value = from_qvariant(value, str) - if self._data.dtype.name == "bool": - try: - val = bool(float(value)) - except ValueError: - val = value.lower() == "true" - elif self._data.dtype.name.startswith("string"): - val = str(value) - elif self._data.dtype.name.startswith("unicode"): - val = to_text_string(value) - else: - if value.lower().startswith('e') or value.lower().endswith('e'): - return False - try: - val = complex(value) - if not val.imag: - val = val.real - except ValueError as e: - QMessageBox.critical(self.dialog, "Error", - "Value error: %s" % str(e)) - return False - try: - self.test_array[0] = val # will raise an Exception eventually - except OverflowError as e: - print(type(e.message)) - QMessageBox.critical(self.dialog, "Error", - "Overflow error: %s" % e.message) - return False - - # Add change to self.changes - self.changes[(i, j)] = val - self.emit(SIGNAL("dataChanged(QModelIndex,QModelIndex)"), - index, index) - if val > self.vmax: - self.vmax = val - if val < self.vmin: - self.vmin = val - return True - - def flags(self, index): - """Set editable flag""" - if not index.isValid(): - return Qt.ItemIsEnabled - return Qt.ItemFlags(QAbstractTableModel.flags(self, index)| - Qt.ItemIsEditable) - - def headerData(self, section, orientation, role=Qt.DisplayRole): - """Set header data""" - if role != Qt.DisplayRole: - return to_qvariant() - labels = self.xlabels if orientation == Qt.Horizontal else self.ylabels - if labels is None: - return to_qvariant(int(section)) - else: - return to_qvariant(labels[section]) - - -class ArrayDelegate(QItemDelegate): - """Array Editor Item Delegate""" - def __init__(self, dtype, parent=None): - QItemDelegate.__init__(self, parent) - self.dtype = dtype - - def createEditor(self, parent, option, index): - """Create editor widget""" - model = index.model() - value = model.get_value(index) - if model._data.dtype.name == "bool": - value = not value - model.setData(index, to_qvariant(value)) - return - elif value is not np.ma.masked: - editor = QLineEdit(parent) - editor.setFont(get_font('arrayeditor')) - editor.setAlignment(Qt.AlignCenter) - if is_number(self.dtype): - editor.setValidator(QDoubleValidator(editor)) - self.connect(editor, SIGNAL("returnPressed()"), - self.commitAndCloseEditor) - return editor - - def commitAndCloseEditor(self): - """Commit and close editor""" - editor = self.sender() - self.emit(SIGNAL("commitData(QWidget*)"), editor) - self.emit(SIGNAL("closeEditor(QWidget*)"), editor) - - def setEditorData(self, editor, index): - """Set editor widget's data""" - text = from_qvariant(index.model().data(index, Qt.DisplayRole), str) - editor.setText(text) - - -#TODO: Implement "Paste" (from clipboard) feature -class ArrayView(QTableView): - """Array view class""" - def __init__(self, parent, model, dtype, shape): - QTableView.__init__(self, parent) - - self.setModel(model) - self.setItemDelegate(ArrayDelegate(dtype, self)) - total_width = 0 - for k in range(shape[1]): - total_width += self.columnWidth(k) - self.viewport().resize(min(total_width, 1024), self.height()) - self.shape = shape - self.menu = self.setup_menu() - new_shortcut(QKeySequence.Copy, self, self.copy) - self.connect(self.horizontalScrollBar(), SIGNAL("valueChanged(int)"), - lambda val: self.load_more_data(val, columns=True)) - self.connect(self.verticalScrollBar(), SIGNAL("valueChanged(int)"), - lambda val: self.load_more_data(val, rows=True)) - - def load_more_data(self, value, rows=False, columns=False): - if rows and value == self.verticalScrollBar().maximum(): - self.model().fetch_more(rows=rows) - if columns and value == self.horizontalScrollBar().maximum(): - self.model().fetch_more(columns=columns) - - def resize_to_contents(self): - """Resize cells to contents""" - QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) - self.resizeColumnsToContents() - self.model().fetch_more(columns=True) - self.resizeColumnsToContents() - QApplication.restoreOverrideCursor() - - def setup_menu(self): - """Setup context menu""" - self.copy_action = create_action(self, _( "Copy"), - shortcut=keybinding("Copy"), - icon=get_icon('editcopy.png'), - triggered=self.copy, - context=Qt.WidgetShortcut) - menu = QMenu(self) - add_actions(menu, [self.copy_action, ]) - return menu - - def contextMenuEvent(self, event): - """Reimplement Qt method""" - self.menu.popup(event.globalPos()) - event.accept() - - def keyPressEvent(self, event): - """Reimplement Qt method""" - if event == QKeySequence.Copy: - self.copy() - else: - QTableView.keyPressEvent(self, event) - - def _sel_to_text(self, cell_range): - """Copy an array portion to a unicode string""" - row_min, row_max, col_min, col_max = get_idx_rect(cell_range) - _data = self.model().get_data() - output = io.StringIO() - np.savetxt(output, - _data[row_min:row_max+1, col_min:col_max+1], - delimiter='\t') - contents = output.getvalue() - output.close() - return contents - - def copy(self): - """Copy text to clipboard""" - cliptxt = self._sel_to_text( self.selectedIndexes() ) - clipboard = QApplication.clipboard() - clipboard.setText(cliptxt) - - -class ArrayEditorWidget(QWidget): - def __init__(self, parent, data, readonly=False, - xlabels=None, ylabels=None): - QWidget.__init__(self, parent) - self.data = data - self.old_data_shape = None - if len(self.data.shape) == 1: - self.old_data_shape = self.data.shape - self.data.shape = (self.data.shape[0], 1) - elif len(self.data.shape) == 0: - self.old_data_shape = self.data.shape - self.data.shape = (1, 1) - - format = SUPPORTED_FORMATS.get(data.dtype.name, '%s') - self.model = ArrayModel(self.data, format=format, xlabels=xlabels, - ylabels=ylabels, readonly=readonly, parent=self) - self.view = ArrayView(self, self.model, data.dtype, data.shape) - - btn_layout = QHBoxLayout() - btn_layout.setAlignment(Qt.AlignLeft) - btn = QPushButton(_( "Format")) - # disable format button for int type - btn.setEnabled(is_float(data.dtype)) - btn_layout.addWidget(btn) - self.connect(btn, SIGNAL("clicked()"), self.change_format) - btn = QPushButton(_( "Resize")) - btn_layout.addWidget(btn) - self.connect(btn, SIGNAL("clicked()"), self.view.resize_to_contents) - bgcolor = QCheckBox(_( 'Background color')) - bgcolor.setChecked(self.model.bgcolor_enabled) - bgcolor.setEnabled(self.model.bgcolor_enabled) - self.connect(bgcolor, SIGNAL("stateChanged(int)"), self.model.bgcolor) - btn_layout.addWidget(bgcolor) - - layout = QVBoxLayout() - layout.addWidget(self.view) - layout.addLayout(btn_layout) - self.setLayout(layout) - - def accept_changes(self): - """Accept changes""" - for (i, j), value in list(self.model.changes.items()): - self.data[i, j] = value - if self.old_data_shape is not None: - self.data.shape = self.old_data_shape - - def reject_changes(self): - """Reject changes""" - if self.old_data_shape is not None: - self.data.shape = self.old_data_shape - - def change_format(self): - """Change display format""" - format, valid = QInputDialog.getText(self, _( 'Format'), - _( "Float formatting"), - QLineEdit.Normal, self.model.get_format()) - if valid: - format = str(format) - try: - format % 1.1 - except: - QMessageBox.critical(self, _("Error"), - _("Format (%s) is incorrect") % format) - return - self.model.set_format(format) - - -class ArrayEditor(QDialog): - """Array Editor Dialog""" - def __init__(self, parent=None): - QDialog.__init__(self, parent) - - # Destroying the C++ object right after closing the dialog box, - # otherwise it may be garbage-collected in another QThread - # (e.g. the editor's analysis thread in Spyder), thus leading to - # a segmentation fault on UNIX or an application crash on Windows - self.setAttribute(Qt.WA_DeleteOnClose) - - self.data = None - self.arraywidget = None - self.stack = None - self.layout = None - # Values for 3d array editor - self.dim_indexes = [{}, {}, {}] - self.last_dim = 0 # Adjust this for changing the startup dimension - - def setup_and_check(self, data, title='', readonly=False, - xlabels=None, ylabels=None): - """ - Setup ArrayEditor: - return False if data is not supported, True otherwise - """ - self.data = data - is_record_array = data.dtype.names is not None - is_masked_array = isinstance(data, np.ma.MaskedArray) - if data.size == 0: - self.error(_("Array is empty")) - return False - if data.ndim > 3: - self.error(_("Arrays with more than 3 dimensions " - "are not supported")) - return False - if xlabels is not None and len(xlabels) != self.data.shape[1]: - self.error(_("The 'xlabels' argument length " - "do no match array column number")) - return False - if ylabels is not None and len(ylabels) != self.data.shape[0]: - self.error(_("The 'ylabels' argument length " - "do no match array row number")) - return False - if not is_record_array: - dtn = data.dtype.name - if dtn not in SUPPORTED_FORMATS and not dtn.startswith('str') \ - and not dtn.startswith('unicode'): - arr = _("%s arrays") % data.dtype.name - self.error(_("%s are currently not supported") % arr) - return False - - self.layout = QGridLayout() - self.setLayout(self.layout) - self.setWindowIcon(get_icon('arredit.png')) - if title: - title = to_text_string(title) + " - " + _("NumPy array") - else: - title = _("Array editor") - if readonly: - title += ' (' + _('read only') + ')' - self.setWindowTitle(title) - self.resize(600, 500) - - # Stack widget - self.stack = QStackedWidget(self) - if is_record_array: - for name in data.dtype.names: - self.stack.addWidget(ArrayEditorWidget(self, data[name], - readonly, xlabels, ylabels)) - elif is_masked_array: - self.stack.addWidget(ArrayEditorWidget(self, data, readonly, - xlabels, ylabels)) - self.stack.addWidget(ArrayEditorWidget(self, data.data, readonly, - xlabels, ylabels)) - self.stack.addWidget(ArrayEditorWidget(self, data.mask, readonly, - xlabels, ylabels)) - elif data.ndim == 3: - pass - else: - self.stack.addWidget(ArrayEditorWidget(self, data, readonly, - xlabels, ylabels)) - self.arraywidget = self.stack.currentWidget() - self.connect(self.stack, SIGNAL('currentChanged(int)'), - self.current_widget_changed) - self.layout.addWidget(self.stack, 1, 0) - - # Buttons configuration - btn_layout = QHBoxLayout() - if is_record_array or is_masked_array or data.ndim == 3: - if is_record_array: - btn_layout.addWidget(QLabel(_("Record array fields:"))) - names = [] - for name in data.dtype.names: - field = data.dtype.fields[name] - text = name - if len(field) >= 3: - title = field[2] - if not is_text_string(title): - title = repr(title) - text += ' - '+title - names.append(text) - else: - names = [_('Masked data'), _('Data'), _('Mask')] - if data.ndim == 3: - # QSpinBox - self.index_spin = QSpinBox(self, keyboardTracking=False) - self.connect(self.index_spin, SIGNAL('valueChanged(int)'), - self.change_active_widget) - # QComboBox - names = [str(i) for i in range(3)] - ra_combo = QComboBox(self) - ra_combo.addItems(names) - self.connect(ra_combo, SIGNAL('currentIndexChanged(int)'), - self.current_dim_changed) - # Adding the widgets to layout - label = QLabel(_("Axis:")) - btn_layout.addWidget(label) - btn_layout.addWidget(ra_combo) - self.shape_label = QLabel() - btn_layout.addWidget(self.shape_label) - label = QLabel(_("Index:")) - btn_layout.addWidget(label) - btn_layout.addWidget(self.index_spin) - self.slicing_label = QLabel() - btn_layout.addWidget(self.slicing_label) - # set the widget to display when launched - self.current_dim_changed(self.last_dim) - else: - ra_combo = QComboBox(self) - self.connect(ra_combo, SIGNAL('currentIndexChanged(int)'), - self.stack.setCurrentIndex) - ra_combo.addItems(names) - btn_layout.addWidget(ra_combo) - if is_masked_array: - label = QLabel(_("Warning: changes are applied separately")) - label.setToolTip(_("For performance reasons, changes applied "\ - "to masked array won't be reflected in "\ - "array's data (and vice-versa).")) - btn_layout.addWidget(label) - btn_layout.addStretch() - bbox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) - self.connect(bbox, SIGNAL("accepted()"), SLOT("accept()")) - self.connect(bbox, SIGNAL("rejected()"), SLOT("reject()")) - btn_layout.addWidget(bbox) - self.layout.addLayout(btn_layout, 2, 0) - - self.setMinimumSize(400, 300) - - # Make the dialog act as a window - self.setWindowFlags(Qt.Window) - - return True - - def current_widget_changed(self, index): - self.arraywidget = self.stack.widget(index) - - def change_active_widget(self, index): - """ - This is implemented for handling negative values in index for - 3d arrays, to give the same behavior as slicing - """ - string_index = [':']*3 - string_index[self.last_dim] = '%i' - self.slicing_label.setText((r"Slicing: [" + ", ".join(string_index) + - "]") % index) - if index < 0: - data_index = self.data.shape[self.last_dim] + index - else: - data_index = index - slice_index = [slice(None)]*3 - slice_index[self.last_dim] = data_index - - stack_index = self.dim_indexes[self.last_dim].get(data_index) - if stack_index == None: - stack_index = self.stack.count() - self.stack.addWidget(ArrayEditorWidget(self, - self.data[slice_index])) - self.dim_indexes[self.last_dim][data_index] = stack_index - self.stack.update() - self.stack.setCurrentIndex(stack_index) - - def current_dim_changed(self, index): - """ - This change the active axis the array editor is plotting over - in 3D - """ - self.last_dim = index - string_size = ['%i']*3 - string_size[index] = '%i' - self.shape_label.setText(('Shape: (' + ', '.join(string_size) + - ') ') % self.data.shape) - if self.index_spin.value() != 0: - self.index_spin.setValue(0) - else: - # this is done since if the value is currently 0 it does not emit - # currentIndexChanged(int) - self.change_active_widget(0) - self.index_spin.setRange(-self.data.shape[index], - self.data.shape[index]-1) - - def accept(self): - """Reimplement Qt method""" - for index in range(self.stack.count()): - self.stack.widget(index).accept_changes() - QDialog.accept(self) - - def get_value(self): - """Return modified array -- this is *not* a copy""" - # It is import to avoid accessing Qt C++ object as it has probably - # already been destroyed, due to the Qt.WA_DeleteOnClose attribute - return self.data - - def error(self, message): - """An error occured, closing the dialog box""" - QMessageBox.critical(self, _("Array editor"), message) - self.setAttribute(Qt.WA_DeleteOnClose) - self.reject() - - def reject(self): - """Reimplement Qt method""" - if self.arraywidget is not None: - for index in range(self.stack.count()): - self.stack.widget(index).reject_changes() - QDialog.reject(self) - - -def test_edit(data, title="", xlabels=None, ylabels=None, - readonly=False, parent=None): - """Test subroutine""" - dlg = ArrayEditor(parent) - if dlg.setup_and_check(data, title, xlabels=xlabels, ylabels=ylabels, - readonly=readonly) and dlg.exec_(): - return dlg.get_value() - else: - import sys - sys.exit() - - -def test(): - """Array editor test""" - _app = qapplication() - - arr = np.array(["kjrekrjkejr"]) - print("out:", test_edit(arr, "string array")) - from spyderlib.py3compat import u - arr = np.array([u("kjrekrjkejr")]) - print("out:", test_edit(arr, "unicode array")) - arr = np.ma.array([[1, 0], [1, 0]], mask=[[True, False], [False, False]]) - print("out:", test_edit(arr, "masked array")) - arr = np.zeros((2, 2), {'names': ('red', 'green', 'blue'), - 'formats': (np.float32, np.float32, np.float32)}) - print("out:", test_edit(arr, "record array")) - arr = np.array([(0, 0.0), (0, 0.0), (0, 0.0)], - dtype=[(('title 1', 'x'), '|i1'), - (('title 2', 'y'), '>f4')]) - print("out:", test_edit(arr, "record array with titles")) - arr = np.random.rand(5, 5) - print("out:", test_edit(arr, "float array", - xlabels=['a', 'b', 'c', 'd', 'e'])) - arr = np.round(np.random.rand(5, 5)*10)+\ - np.round(np.random.rand(5, 5)*10)*1j - print("out:", test_edit(arr, "complex array", - xlabels=np.linspace(-12, 12, 5), - ylabels=np.linspace(-12, 12, 5))) - arr_in = np.array([True, False, True]) - print("in:", arr_in) - arr_out = test_edit(arr_in, "bool array") - print("out:", arr_out) - print(arr_in is arr_out) - arr = np.array([1, 2, 3], dtype="int8") - print("out:", test_edit(arr, "int array")) - arr = np.zeros((3,3,4)) - arr[0,0,0]=1 - arr[0,0,1]=2 - arr[0,0,2]=3 - print("out:", test_edit(arr)) - - -if __name__ == "__main__": - test() diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/widgets/browser.py spyder-3.0.2+dfsg1/spyder/widgets/browser.py --- spyder-2.3.8+dfsg1/spyder/widgets/browser.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/widgets/browser.py 2016-11-16 16:40:01.000000000 +0100 @@ -1,50 +1,82 @@ # -*- coding: utf-8 -*- # -# Copyright © 2009-2010 Pierre Raybaut +# Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) +# (see spyder/__init__.py for details) """Simple web browser widget""" -from spyderlib.qt.QtGui import (QHBoxLayout, QWidget, QVBoxLayout, - QProgressBar, QLabel, QMenu) -from spyderlib.qt.QtWebKit import QWebView, QWebPage, QWebSettings -from spyderlib.qt.QtCore import SIGNAL, QUrl - +# Standard library imports import sys +# Third party imports +from qtpy.QtCore import QUrl, Signal, Slot +from qtpy.QtWidgets import (QFrame, QHBoxLayout, QLabel, QProgressBar, QMenu, + QVBoxLayout, QWidget) +from qtpy.QtWebEngineWidgets import (QWebEnginePage, QWebEngineSettings, + QWebEngineView, WEBENGINE) + # Local imports -from spyderlib.utils.qthelpers import (get_icon, create_action, add_actions, - create_toolbutton, action2button) -from spyderlib.baseconfig import DEV, _ -from spyderlib.widgets.comboboxes import UrlComboBox -from spyderlib.widgets.findreplace import FindReplace -from spyderlib.py3compat import to_text_string, is_text_string +from spyder.config.base import _, DEV +from spyder.py3compat import is_text_string, to_text_string +from spyder.utils.qthelpers import (action2button, add_actions, + create_action, create_toolbutton) +from spyder.utils import icon_manager as ima +from spyder.widgets.comboboxes import UrlComboBox +from spyder.widgets.findreplace import FindReplace + + +class WebPage(QWebEnginePage): + """ + Web page subclass to manage hyperlinks for WebEngine + + Note: This can't be used for WebKit because the + acceptNavigationRequest method has a different + functionality for it. + """ + linkClicked = Signal(QUrl) + + def acceptNavigationRequest(self, url, navigation_type, isMainFrame): + """ + Overloaded method to handle links ourselves + """ + if navigation_type == QWebEnginePage.NavigationTypeLinkClicked: + self.linkClicked.emit(url) + return False + return True -class WebView(QWebView): - """Web page""" +class WebView(QWebEngineView): + """Web view""" def __init__(self, parent): - QWebView.__init__(self, parent) + QWebEngineView.__init__(self, parent) self.zoom_factor = 1. self.zoom_out_action = create_action(self, _("Zoom out"), - icon=get_icon('zoom_out.png'), + icon=ima.icon('zoom_out'), triggered=self.zoom_out) self.zoom_in_action = create_action(self, _("Zoom in"), - icon=get_icon('zoom_in.png'), + icon=ima.icon('zoom_in'), triggered=self.zoom_in) - + if WEBENGINE: + web_page = WebPage(self) + self.setPage(web_page) + def find_text(self, text, changed=True, forward=True, case=False, words=False, regexp=False): """Find text""" - findflag = QWebPage.FindWrapsAroundDocument + if not WEBENGINE: + findflag = QWebEnginePage.FindWrapsAroundDocument + else: + findflag = 0 + if not forward: - findflag = findflag | QWebPage.FindBackward + findflag = findflag | QWebEnginePage.FindBackward if case: - findflag = findflag | QWebPage.FindCaseSensitively - return self.findText(text, findflag) - + findflag = findflag | QWebEnginePage.FindCaseSensitively + + return self.findText(text, QWebEnginePage.FindFlags(findflag)) + def get_selected_text(self): """Return text selected by current text cursor""" return self.selectedText() @@ -78,37 +110,54 @@ def get_zoom_factor(self): """Return zoom factor""" return self.zoom_factor - + + @Slot() def zoom_out(self): """Zoom out""" self.zoom_factor = max(.1, self.zoom_factor-.1) self.apply_zoom_factor() - + + @Slot() def zoom_in(self): """Zoom in""" self.zoom_factor += .1 self.apply_zoom_factor() - #------ QWebView API ------------------------------------------------------- + #------ QWebEngineView API ------------------------------------------------------- def createWindow(self, webwindowtype): import webbrowser webbrowser.open(to_text_string(self.url().toString())) def contextMenuEvent(self, event): menu = QMenu(self) - actions = [self.pageAction(QWebPage.Back), - self.pageAction(QWebPage.Forward), None, - self.pageAction(QWebPage.SelectAll), - self.pageAction(QWebPage.Copy), None, + actions = [self.pageAction(QWebEnginePage.Back), + self.pageAction(QWebEnginePage.Forward), None, + self.pageAction(QWebEnginePage.SelectAll), + self.pageAction(QWebEnginePage.Copy), None, self.zoom_in_action, self.zoom_out_action] - if DEV: + if DEV and not WEBENGINE: settings = self.page().settings() - settings.setAttribute(QWebSettings.DeveloperExtrasEnabled, True) - actions += [None, self.pageAction(QWebPage.InspectElement)] + settings.setAttribute(QWebEngineSettings.DeveloperExtrasEnabled, True) + actions += [None, self.pageAction(QWebEnginePage.InspectElement)] add_actions(menu, actions) menu.popup(event.globalPos()) event.accept() - + + def setHtml(self, html, baseUrl=QUrl()): + """ + Reimplement Qt method to prevent WebEngine to steal focus + when setting html on the page + + Solution taken from + https://bugreports.qt.io/browse/QTBUG-52999 + """ + if WEBENGINE: + self.setEnabled(False) + super(WebView, self).setHtml(html, baseUrl) + self.setEnabled(True) + else: + super(WebView, self).setHtml(html, baseUrl) + class WebBrowser(QWidget): """ @@ -120,14 +169,11 @@ self.home_url = None self.webview = WebView(self) - self.connect(self.webview, SIGNAL("loadFinished(bool)"), - self.load_finished) - self.connect(self.webview, SIGNAL("titleChanged(QString)"), - self.setWindowTitle) - self.connect(self.webview, SIGNAL("urlChanged(QUrl)"), - self.url_changed) + self.webview.loadFinished.connect(self.load_finished) + self.webview.titleChanged.connect(self.setWindowTitle) + self.webview.urlChanged.connect(self.url_changed) - home_button = create_toolbutton(self, icon=get_icon('home.png'), + home_button = create_toolbutton(self, icon=ima.icon('home'), tip=_("Home"), triggered=self.go_home) @@ -136,42 +182,37 @@ pageact2btn = lambda prop: action2button(self.webview.pageAction(prop), parent=self.webview) - refresh_button = pageact2btn(QWebPage.Reload) - stop_button = pageact2btn(QWebPage.Stop) - previous_button = pageact2btn(QWebPage.Back) - next_button = pageact2btn(QWebPage.Forward) + refresh_button = pageact2btn(QWebEnginePage.Reload) + stop_button = pageact2btn(QWebEnginePage.Stop) + previous_button = pageact2btn(QWebEnginePage.Back) + next_button = pageact2btn(QWebEnginePage.Forward) stop_button.setEnabled(False) - self.connect(self.webview, SIGNAL("loadStarted()"), - lambda: stop_button.setEnabled(True)) - self.connect(self.webview, SIGNAL("loadFinished(bool)"), - lambda: stop_button.setEnabled(False)) + self.webview.loadStarted.connect(lambda: stop_button.setEnabled(True)) + self.webview.loadFinished.connect(lambda: stop_button.setEnabled(False)) progressbar = QProgressBar(self) progressbar.setTextVisible(False) progressbar.hide() - self.connect(self.webview, SIGNAL("loadStarted()"), progressbar.show) - self.connect(self.webview, SIGNAL("loadProgress(int)"), - progressbar.setValue) - self.connect(self.webview, SIGNAL("loadFinished(bool)"), - lambda _state: progressbar.hide()) - + self.webview.loadStarted.connect(progressbar.show) + self.webview.loadProgress.connect(progressbar.setValue) + self.webview.loadFinished.connect(lambda _state: progressbar.hide()) + label = QLabel(self.get_label()) - + self.url_combo = UrlComboBox(self) - self.connect(self.url_combo, SIGNAL('valid(bool)'), - self.url_combo_activated) - self.connect(self.webview, SIGNAL("iconChanged()"), self.icon_changed) - + self.url_combo.valid.connect(self.url_combo_activated) + if not WEBENGINE: + self.webview.iconChanged.connect(self.icon_changed) + self.find_widget = FindReplace(self) self.find_widget.set_editor(self.webview) self.find_widget.hide() - find_button = create_toolbutton(self, icon='find.png', + find_button = create_toolbutton(self, icon=ima.icon('find'), tip=_("Find text"), toggled=self.toggle_find_widget) - self.connect(self.find_widget, SIGNAL("visibility_changed(bool)"), - find_button.setChecked) + self.find_widget.visibility_changed.connect(find_button.setChecked) hlayout = QHBoxLayout() for widget in (previous_button, next_button, home_button, find_button, @@ -205,7 +246,8 @@ else: url = url_or_text self.webview.load(url) - + + @Slot() def go_home(self): """Go to home page""" if self.home_url is not None: @@ -236,7 +278,8 @@ self.url_combo.setItemIcon(self.url_combo.currentIndex(), self.webview.icon()) self.setWindowIcon(self.webview.icon()) - + + @Slot(bool) def toggle_find_widget(self, state): if state: self.find_widget.show() @@ -244,15 +287,47 @@ self.find_widget.hide() -def main(): +class FrameWebView(QFrame): + """ + Framed QWebEngineView for UI consistency in Spyder. + """ + linkClicked = Signal(QUrl) + + def __init__(self, parent): + QFrame.__init__(self, parent) + + self._webview = WebView(self) + + layout = QHBoxLayout() + layout.addWidget(self._webview) + layout.setContentsMargins(0, 0, 0, 0) + self.setLayout(layout) + + self.setFrameStyle(QFrame.StyledPanel | QFrame.Sunken) + + if WEBENGINE: + self._webview.page().linkClicked.connect(self.linkClicked) + else: + self._webview.linkClicked.connect(self.linkClicked) + + def __getattr__(self, name): + return getattr(self._webview, name) + + @property + def web_widget(self): + return self._webview + + +def test(): """Run web browser""" - from spyderlib.utils.qthelpers import qapplication - app = qapplication() + from spyder.utils.qthelpers import qapplication + app = qapplication(test_time=8) widget = WebBrowser() widget.show() - widget.set_home_url('http://localhost:7464/') + widget.set_home_url('http://www.google.com/') widget.go_home() sys.exit(app.exec_()) + if __name__ == '__main__': - main() + test() diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/widgets/calltip.py spyder-3.0.2+dfsg1/spyder/widgets/calltip.py --- spyder-2.3.8+dfsg1/spyder/widgets/calltip.py 2015-10-12 02:00:38.000000000 +0200 +++ spyder-3.0.2+dfsg1/spyder/widgets/calltip.py 2016-10-25 02:05:23.000000000 +0200 @@ -14,14 +14,17 @@ # Standard library imports from unicodedata import category -# System library imports -from spyderlib.qt import QtCore, QtGui +# Third party imports +from qtpy.QtCore import QBasicTimer, QCoreApplication, QEvent, Qt +from qtpy.QtGui import QCursor, QPalette +from qtpy.QtWidgets import (QFrame, QLabel, QTextEdit, QPlainTextEdit, QStyle, + QStyleOptionFrame, QStylePainter, QToolTip) # Local imports -from spyderlib.py3compat import to_text_string +from spyder.py3compat import to_text_string -class CallTipWidget(QtGui.QLabel): +class CallTipWidget(QLabel): """ Shows call tips by parsing the current text of Q[Plain]TextEdit. """ @@ -33,24 +36,24 @@ """ Create a call tip manager that is attached to the specified Qt text edit widget. """ - assert isinstance(text_edit, (QtGui.QTextEdit, QtGui.QPlainTextEdit)) - super(CallTipWidget, self).__init__(None, QtCore.Qt.ToolTip) - self.app = QtCore.QCoreApplication.instance() + assert isinstance(text_edit, (QTextEdit, QPlainTextEdit)) + super(CallTipWidget, self).__init__(None, Qt.ToolTip) + self.app = QCoreApplication.instance() self.hide_timer_on = hide_timer_on - self._hide_timer = QtCore.QBasicTimer() + self._hide_timer = QBasicTimer() self._text_edit = text_edit self.setFont(text_edit.document().defaultFont()) - self.setForegroundRole(QtGui.QPalette.ToolTipText) - self.setBackgroundRole(QtGui.QPalette.ToolTipBase) - self.setPalette(QtGui.QToolTip.palette()) + self.setForegroundRole(QPalette.ToolTipText) + self.setBackgroundRole(QPalette.ToolTipBase) + self.setPalette(QToolTip.palette()) - self.setAlignment(QtCore.Qt.AlignLeft) + self.setAlignment(Qt.AlignLeft) self.setIndent(1) - self.setFrameStyle(QtGui.QFrame.NoFrame) + self.setFrameStyle(QFrame.NoFrame) self.setMargin(1 + self.style().pixelMetric( - QtGui.QStyle.PM_ToolTipLabelFrameWidth, None, self)) + QStyle.PM_ToolTipLabelFrameWidth, None, self)) def eventFilter(self, obj, event): """ Reimplemented to hide on certain key presses and on text edit focus @@ -59,24 +62,24 @@ if obj == self._text_edit: etype = event.type() - if etype == QtCore.QEvent.KeyPress: + if etype == QEvent.KeyPress: key = event.key() - if key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return, - QtCore.Qt.Key_Down): + if key in (Qt.Key_Enter, Qt.Key_Return, + Qt.Key_Down): self.hide() - elif key == QtCore.Qt.Key_Escape: + elif key == Qt.Key_Escape: self.hide() return True - elif etype == QtCore.QEvent.FocusOut: + elif etype == QEvent.FocusOut: self.hide() - elif etype == QtCore.QEvent.Enter: + elif etype == QEvent.Enter: if (self._hide_timer.isActive() and - self.app.topLevelAt(QtGui.QCursor.pos()) == self): + self.app.topLevelAt(QCursor.pos()) == self): self._hide_timer.stop() - elif etype == QtCore.QEvent.Leave: + elif etype == QEvent.Leave: self._leave_event_hide() return super(CallTipWidget, self).eventFilter(obj, event) @@ -97,7 +100,7 @@ """ super(CallTipWidget, self).enterEvent(event) if (self._hide_timer.isActive() and - self.app.topLevelAt(QtGui.QCursor.pos()) == self): + self.app.topLevelAt(QCursor.pos()) == self): self._hide_timer.stop() def hideEvent(self, event): @@ -121,10 +124,10 @@ def paintEvent(self, event): """ Reimplemented to paint the background panel. """ - painter = QtGui.QStylePainter(self) - option = QtGui.QStyleOptionFrame() + painter = QStylePainter(self) + option = QStyleOptionFrame() option.initFrom(self) - painter.drawPrimitive(QtGui.QStyle.PE_PanelTipLabel, option) + painter.drawPrimitive(QStyle.PE_PanelTipLabel, option) painter.end() super(CallTipWidget, self).paintEvent(event) @@ -274,7 +277,7 @@ # If Enter events always came after Leave events, we wouldn't need # this check. But on Mac OS, it sometimes happens the other way # around when the tooltip is created. - self.app.topLevelAt(QtGui.QCursor.pos()) != self): + self.app.topLevelAt(QCursor.pos()) != self): self._hide_timer.start(800, self) #------ Signal handlers ---------------------------------------------------- diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/widgets/colors.py spyder-3.0.2+dfsg1/spyder/widgets/colors.py --- spyder-2.3.8+dfsg1/spyder/widgets/colors.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/widgets/colors.py 2016-10-25 02:05:23.000000000 +0200 @@ -1,24 +1,29 @@ # -*- coding: utf-8 -*- - -from spyderlib.qt.QtGui import (QLineEdit, QIcon, QHBoxLayout, QColor, - QPushButton, QColorDialog, QPixmap) -from spyderlib.qt.QtCore import SIGNAL, QSize, Slot, Property +# +# Copyright © Spyder Project Contributors +# Licensed under the terms of the MIT License +# (see spyder/__init__.py for details) + +# Third party imports +from qtpy.QtCore import Property, QSize, Signal, Slot +from qtpy.QtGui import QColor, QIcon, QPixmap +from qtpy.QtWidgets import QColorDialog, QHBoxLayout, QLineEdit, QPushButton # Local imports -from spyderlib.py3compat import is_text_string +from spyder.py3compat import is_text_string class ColorButton(QPushButton): """ Color choosing push button """ - __pyqtSignals__ = ("colorChanged(QColor)",) + colorChanged = Signal(QColor) def __init__(self, parent=None): QPushButton.__init__(self, parent) self.setFixedSize(20, 20) self.setIconSize(QSize(12, 12)) - self.connect(self, SIGNAL("clicked()"), self.choose_color) + self.clicked.connect(self.choose_color) self._color = QColor() def choose_color(self): @@ -35,7 +40,7 @@ def set_color(self, color): if color != self._color: self._color = color - self.emit(SIGNAL("colorChanged(QColor)"), self._color) + self.colorChanged.emit(self._color) pixmap = QPixmap(self.iconSize()) pixmap.fill(color) self.setIcon(QIcon(pixmap)) @@ -69,13 +74,11 @@ QHBoxLayout.__init__(self) assert isinstance(color, QColor) self.lineedit = QLineEdit(color.name(), parent) - self.connect(self.lineedit, SIGNAL("textChanged(QString)"), - self.update_color) + self.lineedit.textChanged.connect(self.update_color) self.addWidget(self.lineedit) self.colorbtn = ColorButton(parent) self.colorbtn.color = color - self.connect(self.colorbtn, SIGNAL("colorChanged(QColor)"), - self.update_text) + self.colorbtn.colorChanged.connect(self.update_text) self.addWidget(self.colorbtn) def update_color(self, text): diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/widgets/comboboxes.py spyder-3.0.2+dfsg1/spyder/widgets/comboboxes.py --- spyder-2.3.8+dfsg1/spyder/widgets/comboboxes.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/widgets/comboboxes.py 2016-10-25 02:05:23.000000000 +0200 @@ -1,65 +1,96 @@ # -*- coding: utf-8 -*- # -# Copyright © 2009-2010 Pierre Raybaut +# Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) +# (see spyder/__init__.py for details) -"""Customized combobox widgets""" +"""Customized combobox widgets.""" # pylint: disable=C0103 # pylint: disable=R0903 # pylint: disable=R0911 # pylint: disable=R0201 -from spyderlib.qt.QtGui import (QComboBox, QFont, QToolTip, QSizePolicy, - QCompleter) -from spyderlib.qt.QtCore import SIGNAL, Qt, QUrl, QTimer +# Standard library imports +import glob +import os import os.path as osp +# Third party imports +from qtpy.QtCore import QEvent, Qt, QTimer, QUrl, Signal +from qtpy.QtGui import QFont +from qtpy.QtWidgets import QComboBox, QCompleter, QSizePolicy, QToolTip + # Local imports -from spyderlib.baseconfig import _ -from spyderlib.py3compat import to_text_string +from spyder.config.base import _ +from spyder.py3compat import to_text_string +from spyder.widgets.helperwidgets import IconLineEdit class BaseComboBox(QComboBox): """Editable combo box base class""" + valid = Signal(bool, bool) + sig_tab_pressed = Signal(bool) + sig_double_tab_pressed = Signal(bool) + def __init__(self, parent): QComboBox.__init__(self, parent) self.setEditable(True) self.setCompleter(QCompleter(self)) + self.numpress = 0 + self.selected_text = self.currentText() + + # --- Qt overrides + def event(self, event): + """Qt Override. + + Filter tab keys and process double tab keys. + """ + if (event.type() == QEvent.KeyPress) and (event.key() == Qt.Key_Tab): + self.sig_tab_pressed.emit(True) + self.numpress += 1 + if self.numpress == 1: + self.presstimer = QTimer.singleShot(400, self.handle_keypress) + return True + return QComboBox.event(self, event) - # --- overrides def keyPressEvent(self, event): - """Handle key press events""" + """Qt Override. + + Handle key press events. + """ if event.key() == Qt.Key_Return or event.key() == Qt.Key_Enter: if self.add_current_text_if_valid(): self.selected() + self.hide_completer() + elif event.key() == Qt.Key_Escape: + self.set_current_text(self.selected_text) + self.hide_completer() else: QComboBox.keyPressEvent(self, event) - def focusOutEvent(self, event): - """Handle focus out event""" - # Calling asynchronously the 'add_current_text' to avoid crash - # https://groups.google.com/group/spyderlib/browse_thread/thread/2257abf530e210bd - QTimer.singleShot(50, self.add_current_text_if_valid) - QComboBox.focusOutEvent(self, event) - # --- own methods + def handle_keypress(self): + """When hitting tab, it handles if single or double tab""" + if self.numpress == 2: + self.sig_double_tab_pressed.emit(True) + self.numpress = 0 + def is_valid(self, qstr): """ Return True if string is valid Return None if validation can't be done """ pass - + def selected(self): """Action to be executed when a valid item has been selected""" - self.emit(SIGNAL('valid(bool)'), True) - + self.valid.emit(True, True) + def add_text(self, text): - """Add text to combo box: add a new item if text is not found in - combo box items""" + """Add text to combo box: add a new item if text is not found in + combo box items.""" index = self.findText(text) while index != -1: self.removeItem(index) @@ -75,18 +106,32 @@ self.setCurrentIndex(0) else: self.setCurrentIndex(0) - + + def set_current_text(self, text): + """Sets the text of the QLineEdit of the QComboBox.""" + self.lineEdit().setText(to_text_string(text)) + def add_current_text(self): """Add current text to combo box history (convenient method)""" - self.add_text(self.currentText()) - + text = self.currentText() + if osp.isdir(text): + if text[-1] == os.sep: + text = text[:-1] + self.add_text(text) + def add_current_text_if_valid(self): """Add current text to combo box history if valid""" valid = self.is_valid(self.currentText()) if valid or valid is None: self.add_current_text() return True - + else: + self.set_current_text(self.selected_text) + + def hide_completer(self): + """Hides the completion widget.""" + self.setCompleter(QCompleter([], self)) + class PatternComboBox(BaseComboBox): """Search pattern combo box""" @@ -106,83 +151,126 @@ """ Editable combo box + Validate """ + def __init__(self, parent): BaseComboBox.__init__(self, parent) - self.setSizeAdjustPolicy(QComboBox.AdjustToMinimumContentsLength) self.font = QFont() - self.connect(self, SIGNAL("editTextChanged(QString)"), self.validate) - self.connect(self, SIGNAL("activated(QString)"), - lambda qstr: self.validate(qstr, editing=False)) - self.set_default_style() + self.selected_text = self.currentText() + + # Widget setup + self.setSizeAdjustPolicy(QComboBox.AdjustToMinimumContentsLength) + + # Signals + self.editTextChanged.connect(self.validate) self.tips = {True: _("Press enter to validate this entry"), False: _('This entry is incorrect')} - + def show_tip(self, tip=""): """Show tip""" QToolTip.showText(self.mapToGlobal(self.pos()), tip, self) - - def set_default_style(self): - """Set widget style to default""" - self.font.setBold(False) - self.setFont(self.font) - self.setStyleSheet("") - self.show_tip() - + def selected(self): """Action to be executed when a valid item has been selected""" BaseComboBox.selected(self) - self.set_default_style() - + self.selected_text = self.currentText() + def validate(self, qstr, editing=True): """Validate entered path""" + if self.selected_text == qstr: + self.valid.emit(True, True) + return + valid = self.is_valid(qstr) - if self.hasFocus() and valid is not None: - self.font.setBold(True) - self.setFont(self.font) + if editing: if valid: - self.setStyleSheet("color:rgb(50, 155, 50);") + self.valid.emit(True, False) else: - self.setStyleSheet("color:rgb(200, 50, 50);") - if editing: - # Combo box text is being modified: invalidate the entry - self.show_tip(self.tips[valid]) - self.emit(SIGNAL('valid(bool)'), False) - else: - # A new item has just been selected - if valid: - self.selected() - else: - self.emit(SIGNAL('valid(bool)'), False) - else: - self.set_default_style() - + self.valid.emit(False, False) + class PathComboBox(EditableComboBox): """ QComboBox handling path locations """ + open_dir = Signal(str) + def __init__(self, parent, adjust_to_contents=False): EditableComboBox.__init__(self, parent) + + # Replace the default lineedit by a custom one with icon display + lineedit = IconLineEdit(self) + + # Widget setup if adjust_to_contents: self.setSizeAdjustPolicy(QComboBox.AdjustToContents) else: self.setSizeAdjustPolicy(QComboBox.AdjustToMinimumContentsLength) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.tips = {True: _("Press enter to validate this path"), - False: _('This path is incorrect.\n' - 'Enter a correct directory path,\n' - 'then press enter to validate')} - + False: ''} + self.setLineEdit(lineedit) + + # Signals + self.sig_tab_pressed.connect(self.tab_complete) + self.sig_double_tab_pressed.connect(self.double_tab_complete) + self.valid.connect(lineedit.update_status) + + # --- Qt overrides + def focusInEvent(self, event): + """Handle focus in event restoring to display the status icon.""" + show_status = getattr(self.lineEdit(), 'show_status_icon', None) + if show_status: + show_status() + QComboBox.focusInEvent(self, event) + + def focusOutEvent(self, event): + """Handle focus out event restoring the last valid selected path.""" + # Calling asynchronously the 'add_current_text' to avoid crash + # https://groups.google.com/group/spyderlib/browse_thread/thread/2257abf530e210bd + if not self.is_valid(): + lineedit = self.lineEdit() + QTimer.singleShot(50, lambda: lineedit.setText(self.selected_text)) + + hide_status = getattr(self.lineEdit(), 'hide_status_icon', None) + if hide_status: + hide_status() + QComboBox.focusOutEvent(self, event) + + # --- Own methods + def _complete_options(self): + """Find available completion options.""" + text = to_text_string(self.currentText()) + opts = glob.glob(text + "*") + opts = sorted([opt for opt in opts if osp.isdir(opt)]) + self.setCompleter(QCompleter(opts, self)) + return opts + + def double_tab_complete(self): + """If several options available a double tab displays options.""" + opts = self._complete_options() + if len(opts) > 1: + self.completer().complete() + + def tab_complete(self): + """ + If there is a single option available one tab completes the option. + """ + opts = self._complete_options() + if len(opts) == 1: + self.set_current_text(opts[0] + os.sep) + self.hide_completer() + def is_valid(self, qstr=None): """Return True if string is valid""" if qstr is None: qstr = self.currentText() - return osp.isdir( to_text_string(qstr) ) - + return osp.isdir(to_text_string(qstr)) + def selected(self): """Action to be executed when a valid item has been selected""" - EditableComboBox.selected(self) - self.emit(SIGNAL("open_dir(QString)"), self.currentText()) + self.selected_text = self.currentText() + self.valid.emit(True, True) + self.open_dir.emit(self.selected_text) class UrlComboBox(PathComboBox): @@ -191,8 +279,8 @@ """ def __init__(self, parent, adjust_to_contents=False): PathComboBox.__init__(self, parent, adjust_to_contents) - self.disconnect(self, SIGNAL("editTextChanged(QString)"), self.validate) - + self.editTextChanged.disconnect(self.validate) + def is_valid(self, qstr=None): """Return True if string is valid""" if qstr is None: @@ -206,6 +294,7 @@ is_package = osp.isdir(path) and osp.isfile(osp.join(path, '__init__.py')) return is_module or is_package + class PythonModulesComboBox(PathComboBox): """ QComboBox handling Python modules or packages path @@ -213,14 +302,14 @@ """ def __init__(self, parent, adjust_to_contents=False): PathComboBox.__init__(self, parent, adjust_to_contents) - + def is_valid(self, qstr=None): """Return True if string is valid""" if qstr is None: qstr = self.currentText() return is_module_or_package(to_text_string(qstr)) - + def selected(self): """Action to be executed when a valid item has been selected""" EditableComboBox.selected(self) - self.emit(SIGNAL("open(QString)"), self.currentText()) + self.open_dir.emit(self.currentText()) diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/widgets/dataframeeditor.py spyder-3.0.2+dfsg1/spyder/widgets/dataframeeditor.py --- spyder-2.3.8+dfsg1/spyder/widgets/dataframeeditor.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/widgets/dataframeeditor.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,624 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright © 2014 Spyder development team -# Licensed under the terms of the New BSD License -# -# DataFrameModel is based on the class ArrayModel from array editor -# and the class DataFrameModel from the pandas project. -# Present in pandas.sandbox.qtpandas in v0.13.1 -# Copyright (c) 2011-2012, Lambda Foundry, Inc. -# and PyData Development Team All rights reserved - -""" -Pandas DataFrame Editor Dialog -""" - -from spyderlib.qt.QtCore import (QAbstractTableModel, Qt, QModelIndex, - SIGNAL, SLOT) -from spyderlib.qt.QtGui import (QDialog, QTableView, QColor, QGridLayout, - QDialogButtonBox, QHBoxLayout, QPushButton, - QCheckBox, QMessageBox, QInputDialog, QCursor, - QLineEdit, QApplication, QMenu, QKeySequence) -from spyderlib.qt.compat import to_qvariant, from_qvariant -from spyderlib.utils.qthelpers import (qapplication, get_icon, create_action, - add_actions, keybinding) - -from spyderlib.baseconfig import _ -from spyderlib.guiconfig import get_font, new_shortcut -from spyderlib.py3compat import io, is_text_string, to_text_string, PY2 -from spyderlib.utils import encoding -from spyderlib.widgets.arrayeditor import get_idx_rect - -from pandas import DataFrame, Series -import numpy as np - -# Supported Numbers and complex numbers -_sup_nr = (float, int, np.int64, np.int32) -_sup_com = (complex, np.complex64, np.complex128) -# Used to convert bool intrance to false since bool('False') will return True -_bool_false = ['false', '0'] - - -LARGE_SIZE = 5e5 -LARGE_NROWS = 1e5 -LARGE_COLS = 60 - - -def bool_false_check(value): - """ - Used to convert bool intrance to false since any string in bool('') - will return True - """ - if value.lower() in _bool_false: - value = '' - return value - - -def global_max(col_vals, index): - """Returns the global maximum and minimum""" - max_col, min_col = zip(*col_vals) - return max(max_col), min(min_col) - - -class DataFrameModel(QAbstractTableModel): - """ DataFrame Table Model""" - - ROWS_TO_LOAD = 500 - COLS_TO_LOAD = 40 - - def __init__(self, dataFrame, format="%.3g", parent=None): - QAbstractTableModel.__init__(self) - self.dialog = parent - self.df = dataFrame - self.df_index = dataFrame.index.tolist() - self.df_header = dataFrame.columns.tolist() - self._format = format - self.complex_intran = None - - self.total_rows = self.df.shape[0] - self.total_cols = self.df.shape[1] - size = self.total_rows * self.total_cols - - huerange = [.66, .99] # Hue - self.sat = .7 # Saturation - self.val = 1. # Value - self.alp = .6 # Alpha-channel - self.hue0 = huerange[0] - self.dhue = huerange[1]-huerange[0] - self.max_min_col = None - if size < LARGE_SIZE: - self.max_min_col_update() - self.colum_avg_enabled = True - self.bgcolor_enabled = True - self.colum_avg(1) - else: - self.colum_avg_enabled = False - self.bgcolor_enabled = False - self.colum_avg(0) - - # Use paging when the total size, number of rows or number of - # columns is too large - if size > LARGE_SIZE: - self.rows_loaded = self.ROWS_TO_LOAD - self.cols_loaded = self.COLS_TO_LOAD - else: - if self.total_rows > LARGE_NROWS: - self.rows_loaded = self.ROWS_TO_LOAD - else: - self.rows_loaded = self.total_rows - if self.total_cols > LARGE_COLS: - self.cols_loaded = self.COLS_TO_LOAD - else: - self.cols_loaded = self.total_cols - - def max_min_col_update(self): - """Determines the maximum and minimum number in each column""" - # If there are no rows to compute max/min then return - if self.df.shape[0] == 0: - return - max_r = self.df.max(numeric_only=True) - min_r = self.df.min(numeric_only=True) - self.max_min_col = list(zip(max_r, min_r)) - if len(self.max_min_col) != self.df.shape[1]: - # Then it contain complex numbers or other types - float_intran = self.df.applymap(lambda e: isinstance(e, _sup_nr)) - self.complex_intran = self.df.applymap(lambda e: - isinstance(e, _sup_com)) - mask = float_intran & (~ self.complex_intran) - try: - df_abs = self.df[self.complex_intran].abs() - except TypeError: - df_abs = self.df[self.complex_intran] - max_c = df_abs.max(skipna=True) - min_c = df_abs.min(skipna=True) - df_real = self.df[mask] - max_r = df_real.max(skipna=True) - min_r = df_real.min(skipna=True) - self.max_min_col = list(zip(DataFrame([max_c, - max_r]).max(skipna=True), - DataFrame([min_c, - min_r]).min(skipna=True))) - self.max_min_col = [[vmax, vmin-1] if vmax == vmin else [vmax, vmin] - for vmax, vmin in self.max_min_col] - - def get_format(self): - """Return current format""" - # Avoid accessing the private attribute _format from outside - return self._format - - def set_format(self, format): - """Change display format""" - self._format = format - self.reset() - - def bgcolor(self, state): - """Toggle backgroundcolor""" - self.bgcolor_enabled = state > 0 - self.reset() - - def colum_avg(self, state): - """Toggle backgroundcolor""" - self.colum_avg_enabled = state > 0 - if self.colum_avg_enabled: - self.return_max = lambda col_vals, index: col_vals[index] - else: - self.return_max = global_max - self.reset() - - def headerData(self, section, orientation, role=Qt.DisplayRole): - """Set header data""" - if role != Qt.DisplayRole: - return to_qvariant() - - if orientation == Qt.Horizontal: - if section == 0: - return 'Index' - elif section == 1 and PY2: - # Get rid of possible BOM utf-8 data present at the - # beginning of a file, which gets attached to the first - # column header when headers are present in the first - # row. - # Fixes Issue 2514 - try: - header = to_text_string(self.df_header[0], - encoding='utf-8-sig') - except: - header = to_text_string(self.df_header[0]) - return to_qvariant(header) - else: - return to_qvariant(to_text_string(self.df_header[section-1])) - else: - return to_qvariant() - - def get_bgcolor(self, index): - """Background color depending on value""" - column = index.column() - if column == 0: - color = QColor(Qt.lightGray) - color.setAlphaF(.8) - return color - if not self.bgcolor_enabled: - return - value = self.get_value(index.row(), column-1) - if isinstance(value, _sup_com): - color_func = abs - else: - color_func = float - if isinstance(value, _sup_nr+_sup_com) and self.bgcolor_enabled: - vmax, vmin = self.return_max(self.max_min_col, column-1) - hue = self.hue0 + self.dhue*(vmax-color_func(value)) / (vmax-vmin) - hue = float(abs(hue)) - color = QColor.fromHsvF(hue, self.sat, self.val, self.alp) - elif is_text_string(value): - color = QColor(Qt.lightGray) - color.setAlphaF(.05) - else: - color = QColor(Qt.lightGray) - color.setAlphaF(.3) - return color - - def get_value(self, row, column): - """Returns the value of the DataFrame""" - # To increase the performance iat is used but that requires error - # handling, so fallback uses iloc - try: - value = self.df.iat[row, column] - except: - value = self.df.iloc[row, column] - return value - - def update_df_index(self): - """"Update the DataFrame index""" - self.df_index = self.df.index.tolist() - - def data(self, index, role=Qt.DisplayRole): - """Cell content""" - if not index.isValid(): - return to_qvariant() - if role == Qt.DisplayRole or role == Qt.EditRole: - column = index.column() - row = index.row() - if column == 0: - return to_qvariant(to_text_string(self.df_index[row])) - else: - value = self.get_value(row, column-1) - if isinstance(value, float): - return to_qvariant(self._format % value) - else: - try: - return to_qvariant(to_text_string(value)) - except UnicodeDecodeError: - return to_qvariant(encoding.to_unicode(value)) - elif role == Qt.BackgroundColorRole: - return to_qvariant(self.get_bgcolor(index)) - elif role == Qt.FontRole: - return to_qvariant(get_font('arrayeditor')) - return to_qvariant() - - def sort(self, column, order=Qt.AscendingOrder): - """Overriding sort method""" - if self.complex_intran is not None: - if self.complex_intran.any(axis=0).iloc[column-1]: - QMessageBox.critical(self.dialog, "Error", - "TypeError error: no ordering " - "relation is defined for complex numbers") - return False - try: - if column > 0: - self.df.sort(columns=self.df.columns[column-1], - ascending=order, inplace=True) - self.update_df_index() - else: - self.df.sort_index(inplace=True, ascending=order) - self.update_df_index() - except TypeError as e: - QMessageBox.critical(self.dialog, "Error", - "TypeError error: %s" % str(e)) - return False - - self.reset() - return True - - def flags(self, index): - """Set flags""" - if index.column() == 0: - return Qt.ItemIsEnabled | Qt.ItemIsSelectable - return Qt.ItemFlags(QAbstractTableModel.flags(self, index) | - Qt.ItemIsEditable) - - def setData(self, index, value, role=Qt.EditRole, change_type=None): - """Cell content change""" - column = index.column() - row = index.row() - - if change_type is not None: - try: - value = self.data(index, role=Qt.DisplayRole) - val = from_qvariant(value, str) - if change_type is bool: - val = bool_false_check(val) - self.df.iloc[row, column - 1] = change_type(val) - except ValueError: - self.df.iloc[row, column - 1] = change_type('0') - else: - val = from_qvariant(value, str) - current_value = self.get_value(row, column-1) - if isinstance(current_value, bool): - val = bool_false_check(val) - if isinstance(current_value, ((bool,) + _sup_nr + _sup_com)) or \ - is_text_string(current_value): - try: - self.df.iloc[row, column-1] = current_value.__class__(val) - except ValueError as e: - QMessageBox.critical(self.dialog, "Error", - "Value error: %s" % str(e)) - return False - else: - QMessageBox.critical(self.dialog, "Error", - "The type of the cell is not a supported " - "type") - return False - self.max_min_col_update() - return True - - def get_data(self): - """Return data""" - return self.df - - def rowCount(self, index=QModelIndex()): - """DataFrame row number""" - if self.total_rows <= self.rows_loaded: - return self.total_rows - else: - return self.rows_loaded - - def can_fetch_more(self, rows=False, columns=False): - if rows: - if self.total_rows > self.rows_loaded: - return True - else: - return False - if columns: - if self.total_cols > self.cols_loaded: - return True - else: - return False - - def fetch_more(self, rows=False, columns=False): - if self.can_fetch_more(rows=rows): - reminder = self.total_rows - self.rows_loaded - items_to_fetch = min(reminder, self.ROWS_TO_LOAD) - self.beginInsertRows(QModelIndex(), self.rows_loaded, - self.rows_loaded + items_to_fetch - 1) - self.rows_loaded += items_to_fetch - self.endInsertRows() - if self.can_fetch_more(columns=columns): - reminder = self.total_cols - self.cols_loaded - items_to_fetch = min(reminder, self.COLS_TO_LOAD) - self.beginInsertColumns(QModelIndex(), self.cols_loaded, - self.cols_loaded + items_to_fetch - 1) - self.cols_loaded += items_to_fetch - self.endInsertColumns() - - def columnCount(self, index=QModelIndex()): - """DataFrame column number""" - # This is done to implement series - if len(self.df.shape) == 1: - return 2 - elif self.total_cols <= self.cols_loaded: - return self.total_cols + 1 - else: - return self.cols_loaded + 1 - - -class DataFrameView(QTableView): - """Data Frame view class""" - def __init__(self, parent, model): - QTableView.__init__(self, parent) - self.setModel(model) - - self.sort_old = [None] - self.header_class = self.horizontalHeader() - self.connect(self.header_class, - SIGNAL("sectionClicked(int)"), self.sortByColumn) - self.menu = self.setup_menu() - new_shortcut(QKeySequence.Copy, self, self.copy) - self.connect(self.horizontalScrollBar(), SIGNAL("valueChanged(int)"), - lambda val: self.load_more_data(val, columns=True)) - self.connect(self.verticalScrollBar(), SIGNAL("valueChanged(int)"), - lambda val: self.load_more_data(val, rows=True)) - - def load_more_data(self, value, rows=False, columns=False): - if rows and value == self.verticalScrollBar().maximum(): - self.model().fetch_more(rows=rows) - if columns and value == self.horizontalScrollBar().maximum(): - self.model().fetch_more(columns=columns) - - def sortByColumn(self, index): - """ Implement a Column sort """ - if self.sort_old == [None]: - self.header_class.setSortIndicatorShown(True) - sort_order = self.header_class.sortIndicatorOrder() - if not self.model().sort(index, sort_order): - if len(self.sort_old) != 2: - self.header_class.setSortIndicatorShown(False) - else: - self.header_class.setSortIndicator(self.sort_old[0], - self.sort_old[1]) - return - self.sort_old = [index, self.header_class.sortIndicatorOrder()] - - def contextMenuEvent(self, event): - """Reimplement Qt method""" - self.menu.popup(event.globalPos()) - event.accept() - - def setup_menu(self): - """Setup context menu""" - copy_action = create_action(self, _( "Copy"), - shortcut=keybinding("Copy"), - icon=get_icon('editcopy.png'), - triggered=self.copy, - context=Qt.WidgetShortcut) - functions = ((_("To bool"), bool), (_("To complex"), complex), - (_("To int"), int), (_("To float"), float), - (_("To str"), to_text_string)) - types_in_menu = [copy_action] - for name, func in functions: - types_in_menu += [create_action(self, name, - triggered=lambda func=func: - self.change_type(func), - context=Qt.WidgetShortcut)] - menu = QMenu(self) - add_actions(menu, types_in_menu) - return menu - - def change_type(self, func): - """A function that changes types of cells""" - model = self.model() - index_list = self.selectedIndexes() - [model.setData(i, '', change_type=func) for i in index_list] - - def copy(self): - """Copy text to clipboard""" - (row_min, row_max, - col_min, col_max) = get_idx_rect(self.selectedIndexes()) - index = header = False - if col_min == 0: - col_min = 1 - index = True - df = self.model().df - if col_max == 0: # To copy indices - contents = '\n'.join(map(str, df.index.tolist()[slice(row_min, - row_max+1)])) - else: # To copy DataFrame - if (col_min == 0 or col_min == 1) and (df.shape[1] == col_max): - header = True - obj = df.iloc[slice(row_min, row_max+1), slice(col_min-1, col_max)] - output = io.StringIO() - obj.to_csv(output, sep='\t', index=index, header=header) - contents = output.getvalue() - output.close() - clipboard = QApplication.clipboard() - clipboard.setText(contents) - - -class DataFrameEditor(QDialog): - """ Data Frame Editor Dialog """ - def __init__(self, parent=None): - QDialog.__init__(self, parent) - # Destroying the C++ object right after closing the dialog box, - # otherwise it may be garbage-collected in another QThread - # (e.g. the editor's analysis thread in Spyder), thus leading to - # a segmentation fault on UNIX or an application crash on Windows - self.setAttribute(Qt.WA_DeleteOnClose) - self.is_series = False - self.layout = None - - def setup_and_check(self, data, title=''): - """ - Setup DataFrameEditor: - return False if data is not supported, True otherwise - """ - self.layout = QGridLayout() - self.setLayout(self.layout) - self.setWindowIcon(get_icon('arredit.png')) - if title: - title = to_text_string(title) + " - %s" % data.__class__.__name__ - else: - title = _("%s editor") % data.__class__.__name__ - if isinstance(data, Series): - self.is_series = True - data = data.to_frame() - - self.setWindowTitle(title) - self.resize(600, 500) - - self.dataModel = DataFrameModel(data, parent=self) - self.dataTable = DataFrameView(self, self.dataModel) - - self.layout.addWidget(self.dataTable) - self.setLayout(self.layout) - self.setMinimumSize(400, 300) - # Make the dialog act as a window - self.setWindowFlags(Qt.Window) - btn_layout = QHBoxLayout() - - btn = QPushButton(_("Format")) - # disable format button for int type - btn_layout.addWidget(btn) - self.connect(btn, SIGNAL("clicked()"), self.change_format) - btn = QPushButton(_('Resize')) - btn_layout.addWidget(btn) - self.connect(btn, SIGNAL("clicked()"), self.resize_to_contents) - - bgcolor = QCheckBox(_('Background color')) - bgcolor.setChecked(self.dataModel.bgcolor_enabled) - bgcolor.setEnabled(self.dataModel.bgcolor_enabled) - self.connect(bgcolor, SIGNAL("stateChanged(int)"), - self.change_bgcolor_enable) - btn_layout.addWidget(bgcolor) - - self.bgcolor_global = QCheckBox(_('Column min/max')) - self.bgcolor_global.setChecked(self.dataModel.colum_avg_enabled) - self.bgcolor_global.setEnabled(not self.is_series and - self.dataModel.bgcolor_enabled) - self.connect(self.bgcolor_global, SIGNAL("stateChanged(int)"), - self.dataModel.colum_avg) - btn_layout.addWidget(self.bgcolor_global) - - btn_layout.addStretch() - bbox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) - self.connect(bbox, SIGNAL("accepted()"), SLOT("accept()")) - self.connect(bbox, SIGNAL("rejected()"), SLOT("reject()")) - btn_layout.addWidget(bbox) - - self.layout.addLayout(btn_layout, 2, 0) - - return True - - def change_bgcolor_enable(self, state): - """ - This is implementet so column min/max is only active when bgcolor is - """ - self.dataModel.bgcolor(state) - self.bgcolor_global.setEnabled(not self.is_series and state > 0) - - def change_format(self): - """Change display format""" - format, valid = QInputDialog.getText(self, _('Format'), - _("Float formatting"), - QLineEdit.Normal, - self.dataModel.get_format()) - if valid: - format = str(format) - try: - format % 1.1 - except: - QMessageBox.critical(self, _("Error"), - _("Format (%s) is incorrect") % format) - return - self.dataModel.set_format(format) - - def get_value(self): - """Return modified Dataframe -- this is *not* a copy""" - # It is import to avoid accessing Qt C++ object as it has probably - # already been destroyed, due to the Qt.WA_DeleteOnClose attribute - df = self.dataModel.get_data() - if self.is_series: - return df.iloc[:, 0] - else: - return df - - def resize_to_contents(self): - QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) - self.dataTable.resizeColumnsToContents() - self.dataModel.fetch_more(columns=True) - self.dataTable.resizeColumnsToContents() - QApplication.restoreOverrideCursor() - - -def test_edit(data, title="", parent=None): - """Test subroutine""" - dlg = DataFrameEditor(parent=parent) - if dlg.setup_and_check(data, title=title) and dlg.exec_(): - return dlg.get_value() - else: - import sys - sys.exit() - - -def test(): - """DataFrame editor test""" - from numpy import nan - - df1 = DataFrame([ - [True, "bool"], - [1+1j, "complex"], - ['test', "string"], - [1.11, "float"], - [1, "int"], - [np.random.rand(3, 3), "Unkown type"], - ["Large value", 100], - ["áéí", "unicode"] - ], - index=['a', 'b', nan, nan, nan, 'c', - "Test global max", 'd'], - columns=[nan, 'Type']) - out = test_edit(df1) - print("out:", out) - out = test_edit(df1.iloc[0]) - print("out:", out) - df1 = DataFrame(np.random.rand(100001, 10)) - # Sorting large DataFrame takes time - df1.sort(columns=[0, 1], inplace=True) - out = test_edit(df1) - print("out:", out) - out = test_edit(Series(np.arange(10))) - print("out:", out) - return out - - -if __name__ == '__main__': - _app = qapplication() - df = test() diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/widgets/dependencies.py spyder-3.0.2+dfsg1/spyder/widgets/dependencies.py --- spyder-2.3.8+dfsg1/spyder/widgets/dependencies.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/widgets/dependencies.py 2016-11-16 17:12:04.000000000 +0100 @@ -1,23 +1,26 @@ # -*- coding: utf-8 -*- # -# Copyright © 2013 Pierre Raybaut +# Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) +# (see spyder/__init__.py for details) -"""Module checking Spyder optional runtime dependencies""" +"""Module checking Spyder runtime dependencies""" -from spyderlib.qt.QtGui import (QDialog, QTableView, QItemDelegate, QColor, - QVBoxLayout, QHBoxLayout, QPushButton, - QApplication, QLabel, QDialogButtonBox) -from spyderlib.qt.QtCore import (Qt, QModelIndex, QAbstractTableModel, SIGNAL, - SLOT) -from spyderlib.qt.compat import to_qvariant +# Standard library imports import sys +# Third party imports +from qtpy.compat import to_qvariant +from qtpy.QtCore import Qt, QModelIndex, QAbstractTableModel +from qtpy.QtGui import QColor +from qtpy.QtWidgets import (QApplication, QDialog, QDialogButtonBox, + QHBoxLayout, QItemDelegate, QLabel, QPushButton, + QTableView, QVBoxLayout) + # Local imports -from spyderlib.baseconfig import _ -from spyderlib.utils.qthelpers import get_icon -from spyderlib import __version__ +from spyder import __version__ +from spyder.config.base import _ +from spyder.utils import icon_manager as ima class DependenciesTableModel(QAbstractTableModel): @@ -84,13 +87,17 @@ elif role == Qt.TextAlignmentRole: return to_qvariant(int(Qt.AlignLeft|Qt.AlignVCenter)) elif role == Qt.BackgroundColorRole: - from spyderlib.dependencies import Dependency + from spyder.dependencies import Dependency status = dep.get_status() if status == Dependency.NOK: color = QColor(Qt.red) color.setAlphaF(.25) return to_qvariant(color) + def reset(self): + self.beginResetModel() + self.endResetModel() + class DependenciesDelegate(QItemDelegate): def __init__(self, parent=None): @@ -124,32 +131,33 @@ def __init__(self, parent): QDialog.__init__(self, parent) self.setWindowTitle("Spyder %s: %s" % (__version__, - _("Optional Dependencies"))) - self.setWindowIcon(get_icon('advanced.png')) + _("Dependencies"))) + self.setWindowIcon(ima.icon('tooloptions')) self.setModal(True) self.view = DependenciesTableView(self, []) - important_mods = ['rope', 'pyflakes', 'IPython', 'matplotlib'] + opt_mods = ['NumPy', 'Matplotlib', 'Pandas', 'SymPy'] self.label = QLabel(_("Spyder depends on several Python modules to " - "provide additional functionality for its " - "plugins. The table below shows the required " + "provide the right functionality for all its " + "panes. The table below shows the required " "and installed versions (if any) of all of " "them.

    " - "Although Spyder can work without any of these " - "modules, it's strongly recommended that at " - "least you try to install %s and " - "%s to have a much better experience.") - % (', '.join(important_mods[:-1]), - important_mods[-1])) + "Note: You can safely use Spyder " + "without the following modules installed: " + "%s and %s.

    " + "Please also note that new " + "dependencies or changed ones will be correctly " + "detected only after Spyder is restarted.") + % (', '.join(opt_mods[:-1]), opt_mods[-1])) self.label.setWordWrap(True) self.label.setAlignment(Qt.AlignJustify) self.label.setContentsMargins(5, 8, 12, 10) btn = QPushButton(_("Copy to clipboard"), ) - self.connect(btn, SIGNAL('clicked()'), self.copy_to_clipboard) + btn.clicked.connect(self.copy_to_clipboard) bbox = QDialogButtonBox(QDialogButtonBox.Ok) - self.connect(bbox, SIGNAL("accepted()"), SLOT("accept()")) + bbox.accepted.connect(self.accept) hlayout = QHBoxLayout() hlayout.addWidget(btn) hlayout.addStretch() @@ -169,13 +177,13 @@ self.view.sortByColumn(0, Qt.DescendingOrder) def copy_to_clipboard(self): - from spyderlib.dependencies import status + from spyder.dependencies import status QApplication.clipboard().setText(status()) def test(): """Run dependency widget test""" - from spyderlib import dependencies + from spyder import dependencies # Test sample dependencies.add("IPython", "Enhanced Python interpreter", ">=0.13") @@ -183,7 +191,7 @@ dependencies.add("sympy", "Symbolic Mathematics", ">=10.0") dependencies.add("foo", "Non-existent module", ">=1.0") - from spyderlib.utils.qthelpers import qapplication + from spyder.utils.qthelpers import qapplication app = qapplication() dlg = DependenciesDialog(None) dlg.set_data(dependencies.DEPENDENCIES) diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/widgets/dicteditor.py spyder-3.0.2+dfsg1/spyder/widgets/dicteditor.py --- spyder-2.3.8+dfsg1/spyder/widgets/dicteditor.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/widgets/dicteditor.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,1460 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright © 2009-2010 Pierre Raybaut -# Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) - -""" -Dictionary Editor Widget and Dialog based on Qt -""" - -#TODO: Multiple selection: open as many editors (array/dict/...) as necessary, -# at the same time - -# pylint: disable=C0103 -# pylint: disable=R0903 -# pylint: disable=R0911 -# pylint: disable=R0201 - -from __future__ import print_function -from spyderlib.qt.QtGui import (QMessageBox, QTableView, QItemDelegate, - QLineEdit, QVBoxLayout, QWidget, QColor, - QDialog, QDateEdit, QDialogButtonBox, QMenu, - QInputDialog, QDateTimeEdit, QApplication, - QKeySequence) -from spyderlib.qt.QtCore import (Qt, QModelIndex, QAbstractTableModel, SIGNAL, - SLOT, QDateTime, Signal) -from spyderlib.qt.compat import to_qvariant, from_qvariant, getsavefilename -from spyderlib.utils.qthelpers import mimedata2url - -import sys -import datetime - -# Local import -from spyderlib.baseconfig import _ -from spyderlib.guiconfig import get_font -from spyderlib.utils.misc import fix_reference_name -from spyderlib.utils.qthelpers import (get_icon, add_actions, create_action, - qapplication) -from spyderlib.widgets.dicteditorutils import (sort_against, get_size, - get_human_readable_type, value_to_display, get_color_name, - is_known_type, FakeObject, Image, ndarray, array, MaskedArray, - unsorted_unique, try_to_eval, datestr_to_datetime, - get_numpy_dtype, is_editable_type, DataFrame, Series) -if ndarray is not FakeObject: - from spyderlib.widgets.arrayeditor import ArrayEditor -if DataFrame is not FakeObject: - from spyderlib.widgets.dataframeeditor import DataFrameEditor -from spyderlib.widgets.texteditor import TextEditor -from spyderlib.widgets.importwizard import ImportWizard -from spyderlib.py3compat import (to_text_string, to_binary_string, - is_text_string, is_binary_string, getcwd, u) - - -LARGE_NROWS = 100 - - -def display_to_value(value, default_value, ignore_errors=True): - """Convert back to value""" - value = from_qvariant(value, to_text_string) - try: - np_dtype = get_numpy_dtype(default_value) - if isinstance(default_value, bool): - # We must test for boolean before NumPy data types - # because `bool` class derives from `int` class - try: - value = bool(float(value)) - except ValueError: - value = value.lower() == "true" - elif np_dtype is not None: - if 'complex' in str(type(default_value)): - value = np_dtype(complex(value)) - else: - value = np_dtype(value) - elif is_binary_string(default_value): - value = to_binary_string(value, 'utf8') - elif is_text_string(default_value): - value = to_text_string(value) - elif isinstance(default_value, complex): - value = complex(value) - elif isinstance(default_value, float): - value = float(value) - elif isinstance(default_value, int): - try: - value = int(value) - except ValueError: - value = float(value) - elif isinstance(default_value, datetime.datetime): - value = datestr_to_datetime(value) - elif isinstance(default_value, datetime.date): - value = datestr_to_datetime(value).date() - elif ignore_errors: - value = try_to_eval(value) - else: - value = eval(value) - except (ValueError, SyntaxError): - if ignore_errors: - value = try_to_eval(value) - else: - return default_value - return value - - -class ProxyObject(object): - """Dictionary proxy to an unknown object""" - def __init__(self, obj): - self.__obj__ = obj - - def __len__(self): - return len(dir(self.__obj__)) - - def __getitem__(self, key): - return getattr(self.__obj__, key) - - def __setitem__(self, key, value): - setattr(self.__obj__, key, value) - - -class ReadOnlyDictModel(QAbstractTableModel): - """DictEditor Read-Only Table Model""" - ROWS_TO_LOAD = 50 - - def __init__(self, parent, data, title="", names=False, truncate=True, - minmax=False, remote=False): - QAbstractTableModel.__init__(self, parent) - if data is None: - data = {} - self.names = names - self.truncate = truncate - self.minmax = minmax - self.remote = remote - self.header0 = None - self._data = None - self.total_rows = None - self.showndata = None - self.keys = None - self.title = to_text_string(title) # in case title is not a string - if self.title: - self.title = self.title + ' - ' - self.sizes = [] - self.types = [] - self.set_data(data) - - def get_data(self): - """Return model data""" - return self._data - - def set_data(self, data, dictfilter=None): - """Set model data""" - self._data = data - - if dictfilter is not None and not self.remote and \ - isinstance(data, (tuple, list, dict)): - data = dictfilter(data) - self.showndata = data - - self.header0 = _("Index") - if self.names: - self.header0 = _("Name") - if isinstance(data, tuple): - self.keys = list(range(len(data))) - self.title += _("Tuple") - elif isinstance(data, list): - self.keys = list(range(len(data))) - self.title += _("List") - elif isinstance(data, dict): - self.keys = list(data.keys()) - self.title += _("Dictionary") - if not self.names: - self.header0 = _("Key") - else: - self.keys = dir(data) - self._data = data = self.showndata = ProxyObject(data) - self.title += _("Object") - if not self.names: - self.header0 = _("Attribute") - - self.title += ' ('+str(len(self.keys))+' '+ _("elements")+')' - - self.total_rows = len(self.keys) - if self.total_rows > LARGE_NROWS: - self.rows_loaded = self.ROWS_TO_LOAD - else: - self.rows_loaded = self.total_rows - - self.set_size_and_type() - self.reset() - - def set_size_and_type(self, start=None, stop=None): - data = self._data - - if start is None and stop is None: - start = 0 - stop = self.rows_loaded - fetch_more = False - else: - fetch_more = True - - if self.remote: - sizes = [ data[self.keys[index]]['size'] - for index in range(start, stop) ] - types = [ data[self.keys[index]]['type'] - for index in range(start, stop) ] - else: - sizes = [ get_size(data[self.keys[index]]) - for index in range(start, stop) ] - types = [ get_human_readable_type(data[self.keys[index]]) - for index in range(start, stop) ] - - if fetch_more: - self.sizes = self.sizes + sizes - self.types = self.types + types - else: - self.sizes = sizes - self.types = types - - def sort(self, column, order=Qt.AscendingOrder): - """Overriding sort method""" - reverse = (order==Qt.DescendingOrder) - if column == 0: - self.sizes = sort_against(self.sizes, self.keys, reverse) - self.types = sort_against(self.types, self.keys, reverse) - try: - self.keys.sort(reverse=reverse) - except: - pass - elif column == 1: - self.keys = sort_against(self.keys, self.types, reverse) - self.sizes = sort_against(self.sizes, self.types, reverse) - try: - self.types.sort(reverse=reverse) - except: - pass - elif column == 2: - self.keys = sort_against(self.keys, self.sizes, reverse) - self.types = sort_against(self.types, self.sizes, reverse) - try: - self.sizes.sort(reverse=reverse) - except: - pass - elif column == 3: - self.keys = sort_against(self.keys, self.sizes, reverse) - self.types = sort_against(self.types, self.sizes, reverse) - try: - self.sizes.sort(reverse=reverse) - except: - pass - elif column == 4: - values = [self._data[key] for key in self.keys] - self.keys = sort_against(self.keys, values, reverse) - self.sizes = sort_against(self.sizes, values, reverse) - self.types = sort_against(self.types, values, reverse) - self.reset() - - def columnCount(self, qindex=QModelIndex()): - """Array column number""" - return 4 - - def rowCount(self, index=QModelIndex()): - """Array row number""" - if self.total_rows <= self.rows_loaded: - return self.total_rows - else: - return self.rows_loaded - - def canFetchMore(self, index=QModelIndex()): - if self.total_rows > self.rows_loaded: - return True - else: - return False - - def fetchMore(self, index=QModelIndex()): - reminder = self.total_rows - self.rows_loaded - items_to_fetch = min(reminder, self.ROWS_TO_LOAD) - self.set_size_and_type(self.rows_loaded, - self.rows_loaded + items_to_fetch) - self.beginInsertRows(QModelIndex(), self.rows_loaded, - self.rows_loaded + items_to_fetch - 1) - self.rows_loaded += items_to_fetch - self.endInsertRows() - - def get_index_from_key(self, key): - try: - return self.createIndex(self.keys.index(key), 0) - except ValueError: - return QModelIndex() - - def get_key(self, index): - """Return current key""" - return self.keys[index.row()] - - def get_value(self, index): - """Return current value""" - if index.column() == 0: - return self.keys[ index.row() ] - elif index.column() == 1: - return self.types[ index.row() ] - elif index.column() == 2: - return self.sizes[ index.row() ] - else: - return self._data[ self.keys[index.row()] ] - - def get_bgcolor(self, index): - """Background color depending on value""" - if index.column() == 0: - color = QColor(Qt.lightGray) - color.setAlphaF(.05) - elif index.column() < 3: - color = QColor(Qt.lightGray) - color.setAlphaF(.2) - else: - color = QColor(Qt.lightGray) - color.setAlphaF(.3) - return color - - def data(self, index, role=Qt.DisplayRole): - """Cell content""" - if not index.isValid(): - return to_qvariant() - value = self.get_value(index) - if index.column() == 3 and self.remote: - value = value['view'] - display = value_to_display(value, - truncate=index.column() == 3 and self.truncate, - minmax=self.minmax) - if role == Qt.DisplayRole: - return to_qvariant(display) - elif role == Qt.EditRole: - return to_qvariant(value_to_display(value)) - elif role == Qt.TextAlignmentRole: - if index.column() == 3: - if len(display.splitlines()) < 3: - return to_qvariant(int(Qt.AlignLeft|Qt.AlignVCenter)) - else: - return to_qvariant(int(Qt.AlignLeft|Qt.AlignTop)) - else: - return to_qvariant(int(Qt.AlignLeft|Qt.AlignVCenter)) - elif role == Qt.BackgroundColorRole: - return to_qvariant( self.get_bgcolor(index) ) - elif role == Qt.FontRole: - if index.column() < 3: - return to_qvariant(get_font('dicteditor_header')) - else: - return to_qvariant(get_font('dicteditor')) - return to_qvariant() - - def headerData(self, section, orientation, role=Qt.DisplayRole): - """Overriding method headerData""" - if role != Qt.DisplayRole: - if role == Qt.FontRole: - return to_qvariant(get_font('dicteditor_header')) - else: - return to_qvariant() - i_column = int(section) - if orientation == Qt.Horizontal: - headers = (self.header0, _("Type"), _("Size"), _("Value")) - return to_qvariant( headers[i_column] ) - else: - return to_qvariant() - - def flags(self, index): - """Overriding method flags""" - # This method was implemented in DictModel only, but to enable tuple - # exploration (even without editing), this method was moved here - if not index.isValid(): - return Qt.ItemIsEnabled - return Qt.ItemFlags(QAbstractTableModel.flags(self, index)| - Qt.ItemIsEditable) - -class DictModel(ReadOnlyDictModel): - """DictEditor Table Model""" - - def set_value(self, index, value): - """Set value""" - self._data[ self.keys[index.row()] ] = value - self.showndata[ self.keys[index.row()] ] = value - self.sizes[index.row()] = get_size(value) - self.types[index.row()] = get_human_readable_type(value) - - def get_bgcolor(self, index): - """Background color depending on value""" - value = self.get_value(index) - if index.column() < 3: - color = ReadOnlyDictModel.get_bgcolor(self, index) - else: - if self.remote: - color_name = value['color'] - else: - color_name = get_color_name(value) - color = QColor(color_name) - color.setAlphaF(.2) - return color - - def setData(self, index, value, role=Qt.EditRole): - """Cell content change""" - if not index.isValid(): - return False - if index.column() < 3: - return False - value = display_to_value(value, self.get_value(index), - ignore_errors=True) - self.set_value(index, value) - self.emit(SIGNAL("dataChanged(QModelIndex,QModelIndex)"), - index, index) - return True - - -class DictDelegate(QItemDelegate): - """DictEditor Item Delegate""" - - def __init__(self, parent=None): - QItemDelegate.__init__(self, parent) - self._editors = {} # keep references on opened editors - - def get_value(self, index): - if index.isValid(): - return index.model().get_value(index) - - def set_value(self, index, value): - if index.isValid(): - index.model().set_value(index, value) - - def show_warning(self, index): - """ - Decide if showing a warning when the user is trying to view - a big variable associated to a Tablemodel index - - This avoids getting the variables' value to know its - size and type, using instead those already computed by - the TableModel. - - The problem is when a variable is too big, it can take a - lot of time just to get its value - """ - try: - val_size = index.model().sizes[index.row()] - val_type = index.model().types[index.row()] - except: - return False - if val_type in ['list', 'tuple', 'dict'] and int(val_size) > 1e5: - return True - else: - return False - - def createEditor(self, parent, option, index): - """Overriding method createEditor""" - if index.column() < 3: - return None - if self.show_warning(index): - answer = QMessageBox.warning(self.parent(), _("Warning"), - _("Opening this variable can be slow\n\n" - "Do you want to continue anyway?"), - QMessageBox.Yes | QMessageBox.No) - if answer == QMessageBox.No: - return None - try: - value = self.get_value(index) - except Exception as msg: - QMessageBox.critical(self.parent(), _("Edit item"), - _("Unable to retrieve data." - "

    Error message:
    %s" - ) % to_text_string(msg)) - return - key = index.model().get_key(index) - readonly = isinstance(value, tuple) or self.parent().readonly \ - or not is_known_type(value) - #---editor = DictEditor - if isinstance(value, (list, tuple, dict)): - editor = DictEditor() - editor.setup(value, key, icon=self.parent().windowIcon(), - readonly=readonly) - self.create_dialog(editor, dict(model=index.model(), editor=editor, - key=key, readonly=readonly)) - return None - #---editor = ArrayEditor - elif isinstance(value, (ndarray, MaskedArray)) \ - and ndarray is not FakeObject: - if value.size == 0: - return None - editor = ArrayEditor(parent) - if not editor.setup_and_check(value, title=key, readonly=readonly): - return - self.create_dialog(editor, dict(model=index.model(), editor=editor, - key=key, readonly=readonly)) - return None - #---showing image - elif isinstance(value, Image) and ndarray is not FakeObject \ - and Image is not FakeObject: - arr = array(value) - if arr.size == 0: - return None - editor = ArrayEditor(parent) - if not editor.setup_and_check(arr, title=key, readonly=readonly): - return - conv_func = lambda arr: Image.fromarray(arr, mode=value.mode) - self.create_dialog(editor, dict(model=index.model(), editor=editor, - key=key, readonly=readonly, - conv=conv_func)) - return None - #--editor = DataFrameEditor - elif isinstance(value, (DataFrame, Series)) \ - and DataFrame is not FakeObject: - editor = DataFrameEditor() - if not editor.setup_and_check(value, title=key): - return - self.create_dialog(editor, dict(model=index.model(), editor=editor, - key=key, readonly=readonly)) - return None - - #---editor = QDateTimeEdit - elif isinstance(value, datetime.datetime): - editor = QDateTimeEdit(value, parent) - editor.setCalendarPopup(True) - editor.setFont(get_font('dicteditor')) - self.connect(editor, SIGNAL("returnPressed()"), - self.commitAndCloseEditor) - return editor - #---editor = QDateEdit - elif isinstance(value, datetime.date): - editor = QDateEdit(value, parent) - editor.setCalendarPopup(True) - editor.setFont(get_font('dicteditor')) - self.connect(editor, SIGNAL("returnPressed()"), - self.commitAndCloseEditor) - return editor - #---editor = QTextEdit - elif is_text_string(value) and len(value)>40: - editor = TextEditor(value, key) - self.create_dialog(editor, dict(model=index.model(), editor=editor, - key=key, readonly=readonly)) - return None - #---editor = QLineEdit - elif is_editable_type(value): - editor = QLineEdit(parent) - editor.setFont(get_font('dicteditor')) - editor.setAlignment(Qt.AlignLeft) - self.connect(editor, SIGNAL("returnPressed()"), - self.commitAndCloseEditor) - return editor - #---editor = DictEditor for an arbitrary object - else: - editor = DictEditor() - editor.setup(value, key, icon=self.parent().windowIcon(), - readonly=readonly) - self.create_dialog(editor, dict(model=index.model(), editor=editor, - key=key, readonly=readonly)) - return None - - def create_dialog(self, editor, data): - self._editors[id(editor)] = data - self.connect(editor, SIGNAL('accepted()'), - lambda eid=id(editor): self.editor_accepted(eid)) - self.connect(editor, SIGNAL('rejected()'), - lambda eid=id(editor): self.editor_rejected(eid)) - editor.show() - - def editor_accepted(self, editor_id): - data = self._editors[editor_id] - if not data['readonly']: - index = data['model'].get_index_from_key(data['key']) - value = data['editor'].get_value() - conv_func = data.get('conv', lambda v: v) - self.set_value(index, conv_func(value)) - self._editors.pop(editor_id) - - def editor_rejected(self, editor_id): - self._editors.pop(editor_id) - - def commitAndCloseEditor(self): - """Overriding method commitAndCloseEditor""" - editor = self.sender() - self.emit(SIGNAL("commitData(QWidget*)"), editor) - self.emit(SIGNAL("closeEditor(QWidget*)"), editor) - - def setEditorData(self, editor, index): - """Overriding method setEditorData - Model --> Editor""" - value = self.get_value(index) - if isinstance(editor, QLineEdit): - if is_binary_string(value): - try: - value = to_text_string(value, 'utf8') - except: - pass - if not is_text_string(value): - value = repr(value) - editor.setText(value) - elif isinstance(editor, QDateEdit): - editor.setDate(value) - elif isinstance(editor, QDateTimeEdit): - editor.setDateTime(QDateTime(value.date(), value.time())) - - def setModelData(self, editor, model, index): - """Overriding method setModelData - Editor --> Model""" - if not hasattr(model, "set_value"): - # Read-only mode - return - - if isinstance(editor, QLineEdit): - value = editor.text() - try: - value = display_to_value(to_qvariant(value), - self.get_value(index), - ignore_errors=False) - except Exception as msg: - raise - QMessageBox.critical(editor, _("Edit item"), - _("Unable to assign data to item." - "

    Error message:
    %s" - ) % str(msg)) - return - elif isinstance(editor, QDateEdit): - qdate = editor.date() - value = datetime.date( qdate.year(), qdate.month(), qdate.day() ) - elif isinstance(editor, QDateTimeEdit): - qdatetime = editor.dateTime() - qdate = qdatetime.date() - qtime = qdatetime.time() - value = datetime.datetime( qdate.year(), qdate.month(), - qdate.day(), qtime.hour(), - qtime.minute(), qtime.second() ) - else: - # Should not happen... - raise RuntimeError("Unsupported editor widget") - self.set_value(index, value) - - -class BaseTableView(QTableView): - """Base dictionary editor table view""" - sig_option_changed = Signal(str, object) - sig_files_dropped = Signal(list) - - def __init__(self, parent): - QTableView.__init__(self, parent) - self.array_filename = None - self.menu = None - self.empty_ws_menu = None - self.paste_action = None - self.copy_action = None - self.edit_action = None - self.plot_action = None - self.hist_action = None - self.imshow_action = None - self.save_array_action = None - self.insert_action = None - self.remove_action = None - self.truncate_action = None - self.minmax_action = None - self.rename_action = None - self.duplicate_action = None - self.delegate = None - self.setAcceptDrops(True) - - def setup_table(self): - """Setup table""" - self.horizontalHeader().setStretchLastSection(True) - self.adjust_columns() - # Sorting columns - self.setSortingEnabled(True) - self.sortByColumn(0, Qt.AscendingOrder) - - def setup_menu(self, truncate, minmax): - """Setup context menu""" - if self.truncate_action is not None: - self.truncate_action.setChecked(truncate) - self.minmax_action.setChecked(minmax) - return - - resize_action = create_action(self, _("Resize rows to contents"), - triggered=self.resizeRowsToContents) - self.paste_action = create_action(self, _("Paste"), - icon=get_icon('editpaste.png'), - triggered=self.paste) - self.copy_action = create_action(self, _("Copy"), - icon=get_icon('editcopy.png'), - triggered=self.copy) - self.edit_action = create_action(self, _("Edit"), - icon=get_icon('edit.png'), - triggered=self.edit_item) - self.plot_action = create_action(self, _("Plot"), - icon=get_icon('plot.png'), - triggered=lambda: self.plot_item('plot')) - self.plot_action.setVisible(False) - self.hist_action = create_action(self, _("Histogram"), - icon=get_icon('hist.png'), - triggered=lambda: self.plot_item('hist')) - self.hist_action.setVisible(False) - self.imshow_action = create_action(self, _("Show image"), - icon=get_icon('imshow.png'), - triggered=self.imshow_item) - self.imshow_action.setVisible(False) - self.save_array_action = create_action(self, _("Save array"), - icon=get_icon('filesave.png'), - triggered=self.save_array) - self.save_array_action.setVisible(False) - self.insert_action = create_action(self, _("Insert"), - icon=get_icon('insert.png'), - triggered=self.insert_item) - self.remove_action = create_action(self, _("Remove"), - icon=get_icon('editdelete.png'), - triggered=self.remove_item) - self.truncate_action = create_action(self, _("Truncate values"), - toggled=self.toggle_truncate) - self.truncate_action.setChecked(truncate) - self.toggle_truncate(truncate) - self.minmax_action = create_action(self, _("Show arrays min/max"), - toggled=self.toggle_minmax) - self.minmax_action.setChecked(minmax) - self.toggle_minmax(minmax) - self.rename_action = create_action(self, _( "Rename"), - icon=get_icon('rename.png'), - triggered=self.rename_item) - self.duplicate_action = create_action(self, _( "Duplicate"), - icon=get_icon('edit_add.png'), - triggered=self.duplicate_item) - menu = QMenu(self) - menu_actions = [self.edit_action, self.plot_action, self.hist_action, - self.imshow_action, self.save_array_action, - self.insert_action, self.remove_action, - self.copy_action, self.paste_action, - None, self.rename_action, self.duplicate_action, - None, resize_action, None, self.truncate_action] - if ndarray is not FakeObject: - menu_actions.append(self.minmax_action) - add_actions(menu, menu_actions) - self.empty_ws_menu = QMenu(self) - add_actions(self.empty_ws_menu, - [self.insert_action, self.paste_action, - None, resize_action]) - return menu - - #------ Remote/local API --------------------------------------------------- - def remove_values(self, keys): - """Remove values from data""" - raise NotImplementedError - - def copy_value(self, orig_key, new_key): - """Copy value""" - raise NotImplementedError - - def new_value(self, key, value): - """Create new value in data""" - raise NotImplementedError - - def is_list(self, key): - """Return True if variable is a list or a tuple""" - raise NotImplementedError - - def get_len(self, key): - """Return sequence length""" - raise NotImplementedError - - def is_array(self, key): - """Return True if variable is a numpy array""" - raise NotImplementedError - - def is_image(self, key): - """Return True if variable is a PIL.Image image""" - raise NotImplementedError - - def is_dict(self, key): - """Return True if variable is a dictionary""" - raise NotImplementedError - - def get_array_shape(self, key): - """Return array's shape""" - raise NotImplementedError - - def get_array_ndim(self, key): - """Return array's ndim""" - raise NotImplementedError - - def oedit(self, key): - """Edit item""" - raise NotImplementedError - - def plot(self, key, funcname): - """Plot item""" - raise NotImplementedError - - def imshow(self, key): - """Show item's image""" - raise NotImplementedError - - def show_image(self, key): - """Show image (item is a PIL image)""" - raise NotImplementedError - #--------------------------------------------------------------------------- - - def refresh_menu(self): - """Refresh context menu""" - index = self.currentIndex() - condition = index.isValid() - self.edit_action.setEnabled( condition ) - self.remove_action.setEnabled( condition ) - self.refresh_plot_entries(index) - - def refresh_plot_entries(self, index): - if index.isValid(): - key = self.model.get_key(index) - is_list = self.is_list(key) - is_array = self.is_array(key) and self.get_len(key) != 0 - condition_plot = (is_array and len(self.get_array_shape(key)) <= 2) - condition_hist = (is_array and self.get_array_ndim(key) == 1) - condition_imshow = condition_plot and self.get_array_ndim(key) == 2 - condition_imshow = condition_imshow or self.is_image(key) - else: - is_array = condition_plot = condition_imshow = is_list \ - = condition_hist = False - self.plot_action.setVisible(condition_plot or is_list) - self.hist_action.setVisible(condition_hist or is_list) - self.imshow_action.setVisible(condition_imshow) - self.save_array_action.setVisible(is_array) - - def adjust_columns(self): - """Resize two first columns to contents""" - for col in range(3): - self.resizeColumnToContents(col) - - def set_data(self, data): - """Set table data""" - if data is not None: - self.model.set_data(data, self.dictfilter) - self.sortByColumn(0, Qt.AscendingOrder) - - def mousePressEvent(self, event): - """Reimplement Qt method""" - if event.button() != Qt.LeftButton: - QTableView.mousePressEvent(self, event) - return - index_clicked = self.indexAt(event.pos()) - if index_clicked.isValid(): - if index_clicked == self.currentIndex() \ - and index_clicked in self.selectedIndexes(): - self.clearSelection() - else: - QTableView.mousePressEvent(self, event) - else: - self.clearSelection() - event.accept() - - def mouseDoubleClickEvent(self, event): - """Reimplement Qt method""" - index_clicked = self.indexAt(event.pos()) - if index_clicked.isValid(): - row = index_clicked.row() - # TODO: Remove hard coded "Value" column number (3 here) - index_clicked = index_clicked.child(row, 3) - self.edit(index_clicked) - else: - event.accept() - - def keyPressEvent(self, event): - """Reimplement Qt methods""" - if event.key() == Qt.Key_Delete: - self.remove_item() - elif event.key() == Qt.Key_F2: - self.rename_item() - elif event == QKeySequence.Copy: - self.copy() - elif event == QKeySequence.Paste: - self.paste() - else: - QTableView.keyPressEvent(self, event) - - def contextMenuEvent(self, event): - """Reimplement Qt method""" - if self.model.showndata: - self.refresh_menu() - self.menu.popup(event.globalPos()) - event.accept() - else: - self.empty_ws_menu.popup(event.globalPos()) - event.accept() - - def dragEnterEvent(self, event): - """Allow user to drag files""" - if mimedata2url(event.mimeData()): - event.accept() - else: - event.ignore() - - def dragMoveEvent(self, event): - """Allow user to move files""" - if mimedata2url(event.mimeData()): - event.setDropAction(Qt.CopyAction) - event.accept() - else: - event.ignore() - - def dropEvent(self, event): - """Allow user to drop supported files""" - urls = mimedata2url(event.mimeData()) - if urls: - event.setDropAction(Qt.CopyAction) - event.accept() - self.sig_files_dropped.emit(urls) - else: - event.ignore() - - def toggle_truncate(self, state): - """Toggle display truncating option""" - self.sig_option_changed.emit('truncate', state) - self.model.truncate = state - - def toggle_minmax(self, state): - """Toggle min/max display for numpy arrays""" - self.sig_option_changed.emit('minmax', state) - self.model.minmax = state - - def edit_item(self): - """Edit item""" - index = self.currentIndex() - if not index.isValid(): - return - # TODO: Remove hard coded "Value" column number (3 here) - self.edit(index.child(index.row(), 3)) - - def remove_item(self): - """Remove item""" - indexes = self.selectedIndexes() - if not indexes: - return - for index in indexes: - if not index.isValid(): - return - one = _("Do you want to remove selected item?") - more = _("Do you want to remove all selected items?") - answer = QMessageBox.question(self, _( "Remove"), - one if len(indexes) == 1 else more, - QMessageBox.Yes | QMessageBox.No) - if answer == QMessageBox.Yes: - idx_rows = unsorted_unique([idx.row() for idx in indexes]) - keys = [ self.model.keys[idx_row] for idx_row in idx_rows ] - self.remove_values(keys) - - def copy_item(self, erase_original=False): - """Copy item""" - indexes = self.selectedIndexes() - if not indexes: - return - idx_rows = unsorted_unique([idx.row() for idx in indexes]) - if len(idx_rows) > 1 or not indexes[0].isValid(): - return - orig_key = self.model.keys[idx_rows[0]] - new_key, valid = QInputDialog.getText(self, _( 'Rename'), _( 'Key:'), - QLineEdit.Normal, orig_key) - if valid and to_text_string(new_key): - new_key = try_to_eval(to_text_string(new_key)) - if new_key == orig_key: - return - self.copy_value(orig_key, new_key) - if erase_original: - self.remove_values([orig_key]) - - def duplicate_item(self): - """Duplicate item""" - self.copy_item() - - def rename_item(self): - """Rename item""" - self.copy_item(True) - - def insert_item(self): - """Insert item""" - index = self.currentIndex() - if not index.isValid(): - row = self.model.rowCount() - else: - row = index.row() - data = self.model.get_data() - if isinstance(data, list): - key = row - data.insert(row, '') - elif isinstance(data, dict): - key, valid = QInputDialog.getText(self, _( 'Insert'), _( 'Key:'), - QLineEdit.Normal) - if valid and to_text_string(key): - key = try_to_eval(to_text_string(key)) - else: - return - else: - return - value, valid = QInputDialog.getText(self, _('Insert'), _('Value:'), - QLineEdit.Normal) - if valid and to_text_string(value): - self.new_value(key, try_to_eval(to_text_string(value))) - - def __prepare_plot(self): - try: - import guiqwt.pyplot #analysis:ignore - return True - except ImportError: - try: - if 'matplotlib' not in sys.modules: - import matplotlib - matplotlib.use("Qt4Agg") - return True - except ImportError: - QMessageBox.warning(self, _("Import error"), - _("Please install matplotlib" - " or guiqwt.")) - - def plot_item(self, funcname): - """Plot item""" - index = self.currentIndex() - if self.__prepare_plot(): - key = self.model.get_key(index) - try: - self.plot(key, funcname) - except (ValueError, TypeError) as error: - QMessageBox.critical(self, _( "Plot"), - _("Unable to plot data." - "

    Error message:
    %s" - ) % str(error)) - - def imshow_item(self): - """Imshow item""" - index = self.currentIndex() - if self.__prepare_plot(): - key = self.model.get_key(index) - try: - if self.is_image(key): - self.show_image(key) - else: - self.imshow(key) - except (ValueError, TypeError) as error: - QMessageBox.critical(self, _( "Plot"), - _("Unable to show image." - "

    Error message:
    %s" - ) % str(error)) - - def save_array(self): - """Save array""" - title = _( "Save array") - if self.array_filename is None: - self.array_filename = getcwd() - self.emit(SIGNAL('redirect_stdio(bool)'), False) - filename, _selfilter = getsavefilename(self, title, - self.array_filename, - _("NumPy arrays")+" (*.npy)") - self.emit(SIGNAL('redirect_stdio(bool)'), True) - if filename: - self.array_filename = filename - data = self.delegate.get_value( self.currentIndex() ) - try: - import numpy as np - np.save(self.array_filename, data) - except Exception as error: - QMessageBox.critical(self, title, - _("Unable to save array" - "

    Error message:
    %s" - ) % str(error)) - def copy(self): - """Copy text to clipboard""" - clipboard = QApplication.clipboard() - clipl = [] - for idx in self.selectedIndexes(): - if not idx.isValid(): - continue - clipl.append(to_text_string(self.delegate.get_value(idx))) - clipboard.setText(u('\n').join(clipl)) - - def import_from_string(self, text, title=None): - """Import data from string""" - data = self.model.get_data() - editor = ImportWizard(self, text, title=title, - contents_title=_("Clipboard contents"), - varname=fix_reference_name("data", - blacklist=list(data.keys()))) - if editor.exec_(): - var_name, clip_data = editor.get_data() - self.new_value(var_name, clip_data) - - def paste(self): - """Import text/data/code from clipboard""" - clipboard = QApplication.clipboard() - cliptext = '' - if clipboard.mimeData().hasText(): - cliptext = to_text_string(clipboard.text()) - if cliptext.strip(): - self.import_from_string(cliptext, title=_("Import from clipboard")) - else: - QMessageBox.warning(self, _( "Empty clipboard"), - _("Nothing to be imported from clipboard.")) - - -class DictEditorTableView(BaseTableView): - """DictEditor table view""" - def __init__(self, parent, data, readonly=False, title="", - names=False, truncate=True, minmax=False): - BaseTableView.__init__(self, parent) - self.dictfilter = None - self.readonly = readonly or isinstance(data, tuple) - DictModelClass = ReadOnlyDictModel if self.readonly else DictModel - self.model = DictModelClass(self, data, title, names=names, - truncate=truncate, minmax=minmax) - self.setModel(self.model) - self.delegate = DictDelegate(self) - self.setItemDelegate(self.delegate) - - self.setup_table() - self.menu = self.setup_menu(truncate, minmax) - - #------ Remote/local API --------------------------------------------------- - def remove_values(self, keys): - """Remove values from data""" - data = self.model.get_data() - for key in sorted(keys, reverse=True): - data.pop(key) - self.set_data(data) - - def copy_value(self, orig_key, new_key): - """Copy value""" - data = self.model.get_data() - data[new_key] = data[orig_key] - self.set_data(data) - - def new_value(self, key, value): - """Create new value in data""" - data = self.model.get_data() - data[key] = value - self.set_data(data) - - def is_list(self, key): - """Return True if variable is a list or a tuple""" - data = self.model.get_data() - return isinstance(data[key], (tuple, list)) - - def get_len(self, key): - """Return sequence length""" - data = self.model.get_data() - return len(data[key]) - - def is_array(self, key): - """Return True if variable is a numpy array""" - data = self.model.get_data() - return isinstance(data[key], (ndarray, MaskedArray)) - - def is_image(self, key): - """Return True if variable is a PIL.Image image""" - data = self.model.get_data() - return isinstance(data[key], Image) - - def is_dict(self, key): - """Return True if variable is a dictionary""" - data = self.model.get_data() - return isinstance(data[key], dict) - - def get_array_shape(self, key): - """Return array's shape""" - data = self.model.get_data() - return data[key].shape - - def get_array_ndim(self, key): - """Return array's ndim""" - data = self.model.get_data() - return data[key].ndim - - def oedit(self, key): - """Edit item""" - data = self.model.get_data() - from spyderlib.widgets.objecteditor import oedit - oedit(data[key]) - - def plot(self, key, funcname): - """Plot item""" - data = self.model.get_data() - import spyderlib.pyplot as plt - plt.figure() - getattr(plt, funcname)(data[key]) - plt.show() - - def imshow(self, key): - """Show item's image""" - data = self.model.get_data() - import spyderlib.pyplot as plt - plt.figure() - plt.imshow(data[key]) - plt.show() - - def show_image(self, key): - """Show image (item is a PIL image)""" - data = self.model.get_data() - data[key].show() - #--------------------------------------------------------------------------- - - def refresh_menu(self): - """Refresh context menu""" - data = self.model.get_data() - index = self.currentIndex() - condition = (not isinstance(data, tuple)) and index.isValid() \ - and not self.readonly - self.edit_action.setEnabled( condition ) - self.remove_action.setEnabled( condition ) - self.insert_action.setEnabled( not self.readonly ) - self.refresh_plot_entries(index) - - def set_filter(self, dictfilter=None): - """Set table dict filter""" - self.dictfilter = dictfilter - - -class DictEditorWidget(QWidget): - """Dictionary Editor Dialog""" - def __init__(self, parent, data, readonly=False, title="", remote=False): - QWidget.__init__(self, parent) - if remote: - self.editor = RemoteDictEditorTableView(self, data, readonly) - else: - self.editor = DictEditorTableView(self, data, readonly, title) - layout = QVBoxLayout() - layout.addWidget(self.editor) - self.setLayout(layout) - - def set_data(self, data): - """Set DictEditor data""" - self.editor.set_data(data) - - def get_title(self): - """Get model title""" - return self.editor.model.title - - -class DictEditor(QDialog): - """Dictionary/List Editor Dialog""" - def __init__(self, parent=None): - QDialog.__init__(self, parent) - - # Destroying the C++ object right after closing the dialog box, - # otherwise it may be garbage-collected in another QThread - # (e.g. the editor's analysis thread in Spyder), thus leading to - # a segmentation fault on UNIX or an application crash on Windows - self.setAttribute(Qt.WA_DeleteOnClose) - - self.data_copy = None - self.widget = None - - def setup(self, data, title='', readonly=False, width=500, - icon='dictedit.png', remote=False, parent=None): - if isinstance(data, dict): - # dictionnary - self.data_copy = data.copy() - datalen = len(data) - elif isinstance(data, (tuple, list)): - # list, tuple - self.data_copy = data[:] - datalen = len(data) - else: - # unknown object - import copy - self.data_copy = copy.deepcopy(data) - datalen = len(dir(data)) - self.widget = DictEditorWidget(self, self.data_copy, title=title, - readonly=readonly, remote=remote) - - layout = QVBoxLayout() - layout.addWidget(self.widget) - self.setLayout(layout) - - # Buttons configuration - buttons = QDialogButtonBox.Ok - if not readonly: - buttons = buttons | QDialogButtonBox.Cancel - bbox = QDialogButtonBox(buttons) - self.connect(bbox, SIGNAL("accepted()"), SLOT("accept()")) - if not readonly: - self.connect(bbox, SIGNAL("rejected()"), SLOT("reject()")) - layout.addWidget(bbox) - - constant = 121 - row_height = 30 - error_margin = 20 - height = constant + row_height*min([15, datalen]) + error_margin - self.resize(width, height) - - self.setWindowTitle(self.widget.get_title()) - if is_text_string(icon): - icon = get_icon(icon) - self.setWindowIcon(icon) - # Make the dialog act as a window - self.setWindowFlags(Qt.Window) - - def get_value(self): - """Return modified copy of dictionary or list""" - # It is import to avoid accessing Qt C++ object as it has probably - # already been destroyed, due to the Qt.WA_DeleteOnClose attribute - return self.data_copy - - -#----Remote versions of DictDelegate and DictEditorTableView -class RemoteDictDelegate(DictDelegate): - """DictEditor Item Delegate""" - def __init__(self, parent=None, get_value_func=None, set_value_func=None): - DictDelegate.__init__(self, parent) - self.get_value_func = get_value_func - self.set_value_func = set_value_func - - def get_value(self, index): - if index.isValid(): - name = index.model().keys[index.row()] - return self.get_value_func(name) - - def set_value(self, index, value): - if index.isValid(): - name = index.model().keys[index.row()] - self.set_value_func(name, value) - - -class RemoteDictEditorTableView(BaseTableView): - """DictEditor table view""" - def __init__(self, parent, data, truncate=True, minmax=False, - get_value_func=None, set_value_func=None, - new_value_func=None, remove_values_func=None, - copy_value_func=None, is_list_func=None, get_len_func=None, - is_array_func=None, is_image_func=None, is_dict_func=None, - get_array_shape_func=None, get_array_ndim_func=None, - oedit_func=None, plot_func=None, imshow_func=None, - is_data_frame_func=None, is_series_func=None, - show_image_func=None, remote_editing=False): - BaseTableView.__init__(self, parent) - - self.remote_editing_enabled = None - - self.remove_values = remove_values_func - self.copy_value = copy_value_func - self.new_value = new_value_func - - self.is_data_frame = is_data_frame_func - self.is_series = is_series_func - self.is_list = is_list_func - self.get_len = get_len_func - self.is_array = is_array_func - self.is_image = is_image_func - self.is_dict = is_dict_func - self.get_array_shape = get_array_shape_func - self.get_array_ndim = get_array_ndim_func - self.oedit = oedit_func - self.plot = plot_func - self.imshow = imshow_func - self.show_image = show_image_func - - self.dictfilter = None - self.model = None - self.delegate = None - self.readonly = False - self.model = DictModel(self, data, names=True, - truncate=truncate, minmax=minmax, - remote=True) - self.setModel(self.model) - self.delegate = RemoteDictDelegate(self, get_value_func, set_value_func) - self.setItemDelegate(self.delegate) - - self.setup_table() - self.menu = self.setup_menu(truncate, minmax) - - def setup_menu(self, truncate, minmax): - """Setup context menu""" - menu = BaseTableView.setup_menu(self, truncate, minmax) - return menu - - def oedit_possible(self, key): - if (self.is_list(key) or self.is_dict(key) - or self.is_array(key) or self.is_image(key) - or self.is_data_frame(key) or self.is_series(key)): - # If this is a remote dict editor, the following avoid - # transfering large amount of data through the socket - return True - - def edit_item(self): - """ - Reimplement BaseTableView's method to edit item - - Some supported data types are directly edited in the remote process, - thus avoiding to transfer large amount of data through the socket from - the remote process to Spyder - """ - if self.remote_editing_enabled: - index = self.currentIndex() - if not index.isValid(): - return - key = self.model.get_key(index) - if self.oedit_possible(key): - # If this is a remote dict editor, the following avoid - # transfering large amount of data through the socket - self.oedit(key) - else: - BaseTableView.edit_item(self) - else: - BaseTableView.edit_item(self) - - -def get_test_data(): - """Create test data""" - import numpy as np - from spyderlib.pil_patch import Image - image = Image.fromarray(np.random.random_integers(255, size=(100, 100)), - mode='P') - testdict = {'d': 1, 'a': np.random.rand(10, 10), 'b': [1, 2]} - testdate = datetime.date(1945, 5, 8) - class Foobar(object): - def __init__(self): - self.text = "toto" - self.testdict = testdict - self.testdate = testdate - foobar = Foobar() - return {'object': foobar, - 'str': 'kjkj kj k j j kj k jkj', - 'unicode': to_text_string('éù', 'utf-8'), - 'list': [1, 3, [sorted, 5, 6], 'kjkj', None], - 'tuple': ([1, testdate, testdict], 'kjkj', None), - 'dict': testdict, - 'float': 1.2233, - 'int': 223, - 'bool': True, - 'array': np.random.rand(10, 10), - 'masked_array': np.ma.array([[1, 0], [1, 0]], - mask=[[True, False], [False, False]]), - '1D-array': np.linspace(-10, 10), - 'empty_array': np.array([]), - 'image': image, - 'date': testdate, - 'datetime': datetime.datetime(1945, 5, 8), - 'complex': 2+1j, - 'complex64': np.complex64(2+1j), - 'int8_scalar': np.int8(8), - 'int16_scalar': np.int16(16), - 'int32_scalar': np.int32(32), - 'bool_scalar': np.bool(8), - 'unsupported1': np.arccos, - 'unsupported2': np.cast, - #1: (1, 2, 3), -5: ("a", "b", "c"), 2.5: np.array((4.0, 6.0, 8.0)), - } - -def test(): - """Dictionary editor test""" - app = qapplication() #analysis:ignore - dialog = DictEditor() - dialog.setup(get_test_data()) - dialog.show() - app.exec_() - print("out:", dialog.get_value()) - -def remote_editor_test(): - """Remote dictionary editor test""" - from spyderlib.plugins.variableexplorer import VariableExplorer - from spyderlib.widgets.externalshell.monitor import make_remote_view - remote = make_remote_view(get_test_data(), VariableExplorer.get_settings()) - from pprint import pprint - pprint(remote) - app = qapplication() - dialog = DictEditor() - dialog.setup(remote, remote=True) - dialog.show() - app.exec_() - if dialog.result(): - print(dialog.get_value()) - -if __name__ == "__main__": - remote_editor_test() diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/widgets/dicteditorutils.py spyder-3.0.2+dfsg1/spyder/widgets/dicteditorutils.py --- spyder-2.3.8+dfsg1/spyder/widgets/dicteditorutils.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/widgets/dicteditorutils.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,320 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright © 2011 Pierre Raybaut -# Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) - -""" -Utilities for the Dictionary Editor Widget and Dialog based on Qt -""" - -from __future__ import print_function - -import re - -# Local imports -from spyderlib.py3compat import (NUMERIC_TYPES, TEXT_TYPES, to_text_string, - is_text_string, is_binary_string, reprlib, - PY2) -from spyderlib.utils import programs -from spyderlib import dependencies -from spyderlib.baseconfig import _ - - -class FakeObject(object): - """Fake class used in replacement of missing modules""" - pass - - -#----Numpy arrays support -try: - from numpy import ndarray, array, matrix, recarray - from numpy.ma import MaskedArray -except ImportError: - ndarray = array = matrix = recarray = MaskedArray = FakeObject # analysis:ignore - - -def get_numpy_dtype(obj): - """Return NumPy data type associated to obj - Return None if NumPy is not available - or if obj is not a NumPy array or scalar""" - if ndarray is not FakeObject: - # NumPy is available - import numpy as np - if isinstance(obj, np.generic) or isinstance(obj, np.ndarray): - # Numpy scalars all inherit from np.generic. - # Numpy arrays all inherit from np.ndarray. - # If we check that we are certain we have one of these - # types then we are less likely to generate an exception below. - try: - return obj.dtype.type - except (AttributeError, RuntimeError): - # AttributeError: some NumPy objects have no dtype attribute - # RuntimeError: happens with NetCDF objects (Issue 998) - return - - -#----Pandas support -PANDAS_REQVER = '>=0.13.1' -dependencies.add('pandas', _("View and edit DataFrames and Series in the " - "Variable Explorer"), - required_version=PANDAS_REQVER) -if programs.is_module_installed('pandas', PANDAS_REQVER): - from pandas import DataFrame, Series -else: - DataFrame = Series = FakeObject # analysis:ignore - - -#----PIL Images support -try: - from spyderlib import pil_patch - Image = pil_patch.Image.Image -except ImportError: - Image = FakeObject # analysis:ignore - - -#----BeautifulSoup support (see Issue 2448) -try: - import bs4 - NavigableString = bs4.element.NavigableString -except ImportError: - NavigableString = FakeObject # analysis:ignore - - -#----Misc. -def address(obj): - """Return object address as a string: ''""" - return "<%s @ %s>" % (obj.__class__.__name__, - hex(id(obj)).upper().replace('X', 'x')) - - -#----Set limits for the amount of elements in the repr of collections -# (lists, dicts, tuples and sets) -CollectionsRepr = reprlib.Repr() -CollectionsRepr.maxlist = 10 -CollectionsRepr.maxdict = 10 -CollectionsRepr.maxtuple = 10 -CollectionsRepr.maxset = 10 - - -#----date and datetime objects support -import datetime -try: - from dateutil.parser import parse as dateparse -except ImportError: - def dateparse(datestr): # analysis:ignore - """Just for 'year, month, day' strings""" - return datetime.datetime( *list(map(int, datestr.split(','))) ) -def datestr_to_datetime(value): - rp = value.rfind('(')+1 - v = dateparse(value[rp:-1]) - print(value, "-->", v) - return v - - -#----Background colors for supported types -ARRAY_COLOR = "#00ff00" -SCALAR_COLOR = "#0000ff" -COLORS = { - bool: "#ff00ff", - NUMERIC_TYPES: SCALAR_COLOR, - list: "#ffff00", - dict: "#00ffff", - tuple: "#c0c0c0", - TEXT_TYPES: "#800000", - (ndarray, - MaskedArray, - matrix, - DataFrame, - Series): ARRAY_COLOR, - Image: "#008000", - datetime.date: "#808000", - } -CUSTOM_TYPE_COLOR = "#7755aa" -UNSUPPORTED_COLOR = "#ffffff" - -def get_color_name(value): - """Return color name depending on value type""" - if not is_known_type(value): - return CUSTOM_TYPE_COLOR - for typ, name in list(COLORS.items()): - if isinstance(value, typ): - return name - else: - np_dtype = get_numpy_dtype(value) - if np_dtype is None or not hasattr(value, 'size'): - return UNSUPPORTED_COLOR - elif value.size == 1: - return SCALAR_COLOR - else: - return ARRAY_COLOR - -def is_editable_type(value): - """Return True if data type is editable with a standard GUI-based editor, - like DictEditor, ArrayEditor, QDateEdit or a simple QLineEdit""" - return get_color_name(value) not in (UNSUPPORTED_COLOR, CUSTOM_TYPE_COLOR) - - -#----Sorting -def sort_against(lista, listb, reverse=False): - """Arrange lista items in the same order as sorted(listb)""" - try: - return [item for _, item in sorted(zip(listb, lista), reverse=reverse)] - except: - return lista - -def unsorted_unique(lista): - """Removes duplicates from lista neglecting its initial ordering""" - return list(set(lista)) - - -#----Display <--> Value -def value_to_display(value, truncate=False, trunc_len=80, minmax=False): - """Convert value for display purpose""" - try: - if isinstance(value, recarray): - fields = value.names - display = 'Field names: ' + ', '.join(fields) - elif minmax and isinstance(value, (ndarray, MaskedArray)): - if value.size == 0: - display = repr(value) - try: - display = 'Min: %r\nMax: %r' % (value.min(), value.max()) - except TypeError: - pass - except ValueError: - # Happens when one of the array cell contains a sequence - pass - elif isinstance(value, (list, tuple, dict, set)): - display = CollectionsRepr.repr(value) - elif isinstance(value, Image): - display = '%s Mode: %s' % (address(value), value.mode) - elif isinstance(value, DataFrame): - cols = value.columns - if PY2 and len(cols) > 0: - # Get rid of possible BOM utf-8 data present at the - # beginning of a file, which gets attached to the first - # column header when headers are present in the first - # row. - # Fixes Issue 2514 - try: - ini_col = to_text_string(cols[0], encoding='utf-8-sig') - except: - ini_col = to_text_string(cols[0]) - cols = [ini_col] + [to_text_string(c) for c in cols[1:]] - else: - cols = [to_text_string(c) for c in cols] - display = 'Column names: ' + ', '.join(list(cols)) - elif isinstance(value, NavigableString): - # Fixes Issue 2448 - display = to_text_string(value) - elif is_binary_string(value): - try: - display = to_text_string(value, 'utf8') - except: - pass - elif is_text_string(value): - display = value - else: - display = repr(value) - if truncate and len(display) > trunc_len: - display = display[:trunc_len].rstrip() + ' ...' - except: - display = to_text_string(type(value)) - - return display - - -def try_to_eval(value): - """Try to eval value""" - try: - return eval(value) - except (NameError, SyntaxError, ImportError): - return value - -def get_size(item): - """Return size of an item of arbitrary type""" - if isinstance(item, (list, tuple, dict)): - return len(item) - elif isinstance(item, (ndarray, MaskedArray)): - return item.shape - elif isinstance(item, Image): - return item.size - if isinstance(item, (DataFrame, Series)): - return item.shape - else: - return 1 - -def get_type_string(item): - """Return type string of an object""" - if isinstance(item, DataFrame): - return "DataFrame" - if isinstance(item, Series): - return "Series" - found = re.findall(r"<(?:type|class) '(\S*)'>", str(type(item))) - if found: - return found[0] - - -def is_known_type(item): - """Return True if object has a known type""" - # Unfortunately, the masked array case is specific - return isinstance(item, MaskedArray) or get_type_string(item) is not None - -def get_human_readable_type(item): - """Return human-readable type string of an item""" - if isinstance(item, (ndarray, MaskedArray)): - return item.dtype.name - elif isinstance(item, Image): - return "Image" - else: - text = get_type_string(item) - if text is None: - text = to_text_string('unknown') - else: - return text[text.find('.')+1:] - - -#----Globals filter: filter namespace dictionaries (to be edited in DictEditor) -def is_supported(value, check_all=False, filters=None, iterate=True): - """Return True if the value is supported, False otherwise""" - assert filters is not None - if not is_editable_type(value): - return False - elif not isinstance(value, filters): - return False - elif iterate: - if isinstance(value, (list, tuple, set)): - for val in value: - if not is_supported(val, filters=filters, iterate=check_all): - return False - if not check_all: - break - elif isinstance(value, dict): - for key, val in list(value.items()): - if not is_supported(key, filters=filters, iterate=check_all) \ - or not is_supported(val, filters=filters, - iterate=check_all): - return False - if not check_all: - break - return True - -def globalsfilter(input_dict, check_all=False, filters=None, - exclude_private=None, exclude_capitalized=None, - exclude_uppercase=None, exclude_unsupported=None, - excluded_names=None): - """Keep only objects that can be pickled""" - output_dict = {} - for key, value in list(input_dict.items()): - excluded = (exclude_private and key.startswith('_')) or \ - (exclude_capitalized and key[0].isupper()) or \ - (exclude_uppercase and key.isupper() - and len(key) > 1 and not key[1:].isdigit()) or \ - (key in excluded_names) or \ - (exclude_unsupported and \ - not is_supported(value, check_all=check_all, - filters=filters)) - if not excluded: - output_dict[key] = value - return output_dict diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/widgets/editor.py spyder-3.0.2+dfsg1/spyder/widgets/editor.py --- spyder-2.3.8+dfsg1/spyder/widgets/editor.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/widgets/editor.py 2016-11-19 01:31:58.000000000 +0100 @@ -1,2429 +1,2414 @@ -# -*- coding: utf-8 -*- -# -# Copyright © 2009-2011 Pierre Raybaut -# Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) - -"""Editor Widget""" - -# pylint: disable=C0103 -# pylint: disable=R0903 -# pylint: disable=R0911 -# pylint: disable=R0201 - -from __future__ import print_function - -from spyderlib.qt import is_pyqt46 -from spyderlib.qt.QtGui import (QVBoxLayout, QMessageBox, QMenu, QFont, - QAction, QApplication, QWidget, QHBoxLayout, - QLabel, QKeySequence, QMainWindow, - QSplitter, QListWidget, QListWidgetItem, - QDialog, QLineEdit) -from spyderlib.qt.QtCore import (SIGNAL, Qt, QFileInfo, QThread, QObject, - QByteArray, QSize, QPoint, QTimer, Slot) -from spyderlib.qt.compat import getsavefilename - -import os -import sys -import os.path as osp - -# Local imports -from spyderlib.utils import encoding, sourcecode, codeanalysis -from spyderlib.utils import introspection -from spyderlib.baseconfig import _, DEBUG, STDOUT, STDERR -from spyderlib.config import EDIT_FILTERS, EDIT_EXT, get_filter, EDIT_FILETYPES -from spyderlib.guiconfig import create_shortcut, new_shortcut -from spyderlib.utils.qthelpers import (get_icon, create_action, add_actions, - mimedata2url, get_filetype_icon, - create_toolbutton) -from spyderlib.widgets.tabs import BaseTabs -from spyderlib.widgets.findreplace import FindReplace -from spyderlib.widgets.editortools import OutlineExplorerWidget -from spyderlib.widgets.status import (ReadWriteStatus, EOLStatus, - EncodingStatus, CursorPositionStatus) -from spyderlib.widgets.sourcecode import syntaxhighlighters, codeeditor -from spyderlib.widgets.sourcecode.base import TextEditBaseWidget #analysis:ignore -from spyderlib.widgets.sourcecode.codeeditor import Printer #analysis:ignore -from spyderlib.widgets.sourcecode.codeeditor import get_file_language -from spyderlib.py3compat import to_text_string, qbytearray_to_str, u - - -DEBUG_EDITOR = DEBUG >= 3 - - -class FileListDialog(QDialog): - def __init__(self, parent, tabs, fullpath_sorting): - QDialog.__init__(self, parent) - - # Destroying the C++ object right after closing the dialog box, - # otherwise it may be garbage-collected in another QThread - # (e.g. the editor's analysis thread in Spyder), thus leading to - # a segmentation fault on UNIX or an application crash on Windows - self.setAttribute(Qt.WA_DeleteOnClose) - - self.indexes = None - - self.setWindowIcon(get_icon('filelist.png')) - self.setWindowTitle(_("File list management")) - - self.setModal(True) - - flabel = QLabel(_("Filter:")) - self.edit = QLineEdit(self) - self.connect(self.edit, SIGNAL("returnPressed()"), self.edit_file) - self.connect(self.edit, SIGNAL("textChanged(QString)"), - lambda text: self.synchronize(0)) - fhint = QLabel(_("(press Enter to edit file)")) - edit_layout = QHBoxLayout() - edit_layout.addWidget(flabel) - edit_layout.addWidget(self.edit) - edit_layout.addWidget(fhint) - - self.listwidget = QListWidget(self) - self.listwidget.setResizeMode(QListWidget.Adjust) - self.connect(self.listwidget, SIGNAL("itemSelectionChanged()"), - self.item_selection_changed) - self.connect(self.listwidget, SIGNAL("itemActivated(QListWidgetItem*)"), - self.edit_file) - - btn_layout = QHBoxLayout() - edit_btn = create_toolbutton(self, icon=get_icon('edit.png'), - text=_("&Edit file"), autoraise=False, - triggered=self.edit_file, text_beside_icon=True) - edit_btn.setMinimumHeight(28) - btn_layout.addWidget(edit_btn) - - btn_layout.addStretch() - btn_layout.addSpacing(150) - btn_layout.addStretch() - - close_btn = create_toolbutton(self, text=_("&Close file"), - icon=get_icon("fileclose.png"), - autoraise=False, text_beside_icon=True, - triggered=lambda: self.emit(SIGNAL("close_file(int)"), - self.indexes[self.listwidget.currentRow()])) - close_btn.setMinimumHeight(28) - btn_layout.addWidget(close_btn) - - hint = QLabel(_("Hint: press Alt to show accelerators")) - hint.setAlignment(Qt.AlignCenter) - - vlayout = QVBoxLayout() - vlayout.addLayout(edit_layout) - vlayout.addWidget(self.listwidget) - vlayout.addLayout(btn_layout) - vlayout.addWidget(hint) - self.setLayout(vlayout) - - self.tabs = tabs - self.fullpath_sorting = fullpath_sorting - self.buttons = (edit_btn, close_btn) - - def edit_file(self): - row = self.listwidget.currentRow() - if self.listwidget.count() > 0 and row >= 0: - self.emit(SIGNAL("edit_file(int)"), self.indexes[row]) - self.accept() - - def item_selection_changed(self): - for btn in self.buttons: - btn.setEnabled(self.listwidget.currentRow() >= 0) - - def synchronize(self, stack_index): - count = self.tabs.count() - if count == 0: - self.accept() - return - self.listwidget.setTextElideMode(Qt.ElideMiddle - if self.fullpath_sorting - else Qt.ElideRight) - current_row = self.listwidget.currentRow() - if current_row >= 0: - current_text = to_text_string(self.listwidget.currentItem().text()) - else: - current_text = "" - self.listwidget.clear() - self.indexes = [] - new_current_index = stack_index - filter_text = to_text_string(self.edit.text()) - lw_index = 0 - for index in range(count): - text = to_text_string(self.tabs.tabText(index)) - if len(filter_text) == 0 or filter_text in text: - if text == current_text: - new_current_index = lw_index - lw_index += 1 - item = QListWidgetItem(self.tabs.tabIcon(index), - text, self.listwidget) - item.setSizeHint(QSize(0, 25)) - self.listwidget.addItem(item) - self.indexes.append(index) - if new_current_index < self.listwidget.count(): - self.listwidget.setCurrentRow(new_current_index) - for btn in self.buttons: - btn.setEnabled(lw_index > 0) - - -class AnalysisThread(QThread): - """Analysis thread""" - def __init__(self, parent, checker, source_code): - super(AnalysisThread, self).__init__(parent) - self.checker = checker - self.results = None - self.source_code = source_code - - def run(self): - """Run analysis""" - try: - self.results = self.checker(self.source_code) - except Exception: - if DEBUG_EDITOR: - import traceback - traceback.print_exc(file=STDERR) - - -class ThreadManager(QObject): - """Analysis thread manager""" - def __init__(self, parent, max_simultaneous_threads=2): - super(ThreadManager, self).__init__(parent) - self.max_simultaneous_threads = max_simultaneous_threads - self.started_threads = {} - self.pending_threads = [] - self.end_callbacks = {} - - def close_threads(self, parent): - """Close threads associated to parent_id""" - if DEBUG_EDITOR: - print("Call to 'close_threads'", file=STDOUT) - if parent is None: - # Closing all threads - self.pending_threads = [] - threadlist = [] - for threads in list(self.started_threads.values()): - threadlist += threads - else: - parent_id = id(parent) - self.pending_threads = [(_th, _id) for (_th, _id) - in self.pending_threads - if _id != parent_id] - threadlist = self.started_threads.get(parent_id, []) - for thread in threadlist: - if DEBUG_EDITOR: - print("Waiting for thread %r to finish" % thread, file=STDOUT) - while thread.isRunning(): - # We can't terminate thread safely, so we simply wait... - QApplication.processEvents() - - def close_all_threads(self): - """Close all threads""" - if DEBUG_EDITOR: - print("Call to 'close_all_threads'", file=STDOUT) - self.close_threads(None) - - def add_thread(self, checker, end_callback, source_code, parent): - """Add thread to queue""" - parent_id = id(parent) - thread = AnalysisThread(self, checker, source_code) - self.end_callbacks[id(thread)] = end_callback - self.pending_threads.append((thread, parent_id)) - if DEBUG_EDITOR: - print("Added thread %r to queue" % thread, file=STDOUT) - QTimer.singleShot(50, self.update_queue) - - def update_queue(self): - """Update queue""" - started = 0 - for parent_id, threadlist in list(self.started_threads.items()): - still_running = [] - for thread in threadlist: - if thread.isFinished(): - end_callback = self.end_callbacks.pop(id(thread)) - if thread.results is not None: - # The thread was executed successfully - end_callback(thread.results) - thread.setParent(None) - thread = None - else: - still_running.append(thread) - started += 1 - threadlist = None - if still_running: - self.started_threads[parent_id] = still_running - else: - self.started_threads.pop(parent_id) - if DEBUG_EDITOR: - print("Updating queue:", file=STDOUT) - print(" started:", started, file=STDOUT) - print(" pending:", len(self.pending_threads), file=STDOUT) - if self.pending_threads and started < self.max_simultaneous_threads: - thread, parent_id = self.pending_threads.pop(0) - self.connect(thread, SIGNAL('finished()'), self.update_queue) - threadlist = self.started_threads.get(parent_id, []) - self.started_threads[parent_id] = threadlist+[thread] - if DEBUG_EDITOR: - print("===>starting:", thread, file=STDOUT) - thread.start() - - -class FileInfo(QObject): - """File properties""" - def __init__(self, filename, encoding, editor, new, threadmanager, - introspection_plugin): - QObject.__init__(self) - self.threadmanager = threadmanager - self.filename = filename - self.newly_created = new - self.default = False # Default untitled file - self.encoding = encoding - self.editor = editor - self.path = [] - - self.classes = (filename, None, None) - self.analysis_results = [] - self.todo_results = [] - self.lastmodified = QFileInfo(filename).lastModified() - - self.connect(self.editor, SIGNAL('textChanged()'), - self.text_changed) - self.connect(self.editor, SIGNAL('breakpoints_changed()'), - self.breakpoints_changed) - - self.pyflakes_results = None - self.pep8_results = None - - def text_changed(self): - """Editor's text has changed""" - self.default = False - self.emit(SIGNAL('text_changed_at(QString,int)'), - self.filename, self.editor.get_position('cursor')) - - def get_source_code(self): - """Return associated editor source code""" - return to_text_string(self.editor.toPlainText()) - - def run_code_analysis(self, run_pyflakes, run_pep8): - """Run code analysis""" - run_pyflakes = run_pyflakes and codeanalysis.is_pyflakes_installed() - run_pep8 = run_pep8 and\ - codeanalysis.get_checker_executable('pep8') is not None - self.pyflakes_results = [] - self.pep8_results = [] - if self.editor.is_python(): - enc = self.encoding.replace('-guessed', '').replace('-bom', '') - source_code = self.get_source_code().encode(enc) - if run_pyflakes: - self.pyflakes_results = None - if run_pep8: - self.pep8_results = None - if run_pyflakes: - self.threadmanager.add_thread(codeanalysis.check_with_pyflakes, - self.pyflakes_analysis_finished, - source_code, self) - if run_pep8: - self.threadmanager.add_thread(codeanalysis.check_with_pep8, - self.pep8_analysis_finished, - source_code, self) - - def pyflakes_analysis_finished(self, results): - """Pyflakes code analysis thread has finished""" - self.pyflakes_results = results - if self.pep8_results is not None: - self.code_analysis_finished() - - def pep8_analysis_finished(self, results): - """Pep8 code analysis thread has finished""" - self.pep8_results = results - if self.pyflakes_results is not None: - self.code_analysis_finished() - - def code_analysis_finished(self): - """Code analysis thread has finished""" - self.set_analysis_results(self.pyflakes_results+self.pep8_results) - self.emit(SIGNAL('analysis_results_changed()')) - - def set_analysis_results(self, results): - """Set analysis results and update warning markers in editor""" - self.analysis_results = results - self.editor.process_code_analysis(results) - - def cleanup_analysis_results(self): - """Clean-up analysis results""" - self.analysis_results = [] - self.editor.cleanup_code_analysis() - - def run_todo_finder(self): - """Run TODO finder""" - if self.editor.is_python(): - self.threadmanager.add_thread(codeanalysis.find_tasks, - self.todo_finished, - self.get_source_code(), self) - - def todo_finished(self, results): - """Code analysis thread has finished""" - self.set_todo_results(results) - self.emit(SIGNAL('todo_results_changed()')) - - def set_todo_results(self, results): - """Set TODO results and update markers in editor""" - self.todo_results = results - self.editor.process_todo(results) - - def cleanup_todo_results(self): - """Clean-up TODO finder results""" - self.todo_results = [] - - def breakpoints_changed(self): - """Breakpoint list has changed""" - breakpoints = self.editor.get_breakpoints() - if self.editor.breakpoints != breakpoints: - self.editor.breakpoints = breakpoints - self.emit(SIGNAL("save_breakpoints(QString,QString)"), - self.filename, repr(breakpoints)) - - -class EditorStack(QWidget): - def __init__(self, parent, actions): - QWidget.__init__(self, parent) - - self.setAttribute(Qt.WA_DeleteOnClose) - - self.threadmanager = ThreadManager(self) - - self.newwindow_action = None - self.horsplit_action = None - self.versplit_action = None - self.close_action = None - self.__get_split_actions() - - layout = QVBoxLayout() - layout.setContentsMargins(0, 0, 0, 0) - self.setLayout(layout) - - self.menu = None - self.filelist_dlg = None -# self.filelist_btn = None -# self.previous_btn = None -# self.next_btn = None - self.tabs = None - - self.stack_history = [] - - self.setup_editorstack(parent, layout) - - self.find_widget = None - - self.data = [] - - filelist_action = create_action(self, _("File list management"), - icon=get_icon('filelist.png'), - triggered=self.open_filelistdialog) - copy_to_cb_action = create_action(self, _("Copy path to clipboard"), - icon="editcopy.png", - triggered=lambda: - QApplication.clipboard().setText(self.get_current_filename())) - self.menu_actions = actions+[None, filelist_action, copy_to_cb_action] - self.outlineexplorer = None - self.inspector = None - self.unregister_callback = None - self.is_closable = False - self.new_action = None - self.open_action = None - self.save_action = None - self.revert_action = None - self.tempfile_path = None - self.title = _("Editor") - self.pyflakes_enabled = True - self.pep8_enabled = False - self.todolist_enabled = True - self.realtime_analysis_enabled = False - self.is_analysis_done = False - self.linenumbers_enabled = True - self.blanks_enabled = False - self.edgeline_enabled = True - self.edgeline_column = 79 - self.codecompletion_auto_enabled = True - self.codecompletion_case_enabled = False - self.codecompletion_enter_enabled = False - self.calltips_enabled = True - self.go_to_definition_enabled = True - self.close_parentheses_enabled = True - self.close_quotes_enabled = True - self.add_colons_enabled = True - self.auto_unindent_enabled = True - self.indent_chars = " "*4 - self.tab_stop_width = 40 - self.inspector_enabled = False - self.default_font = None - self.wrap_enabled = False - self.tabmode_enabled = False - self.intelligent_backspace_enabled = True - self.highlight_current_line_enabled = False - self.highlight_current_cell_enabled = False - self.occurence_highlighting_enabled = True - self.checkeolchars_enabled = True - self.always_remove_trailing_spaces = False - self.fullpath_sorting_enabled = None - self.focus_to_editor = True - self.set_fullpath_sorting_enabled(False) - ccs = 'Spyder' - if ccs not in syntaxhighlighters.COLOR_SCHEME_NAMES: - ccs = syntaxhighlighters.COLOR_SCHEME_NAMES[0] - self.color_scheme = ccs - self.introspector = introspection.PluginManager(self) - - self.introspector.send_to_inspector.connect(self.send_to_inspector) - self.introspector.edit_goto.connect( - lambda fname, lineno, name: - self.emit(SIGNAL("edit_goto(QString,int,QString)"), - fname, lineno, name)) - - self.__file_status_flag = False - - # Real-time code analysis - self.analysis_timer = QTimer(self) - self.analysis_timer.setSingleShot(True) - self.analysis_timer.setInterval(2000) - self.connect(self.analysis_timer, SIGNAL("timeout()"), - self.analyze_script) - - # Accepting drops - self.setAcceptDrops(True) - - # Local shortcuts - self.shortcuts = self.create_shortcuts() - - def create_shortcuts(self): - """Create local shortcuts""" - # Configurable shortcuts - inspect = create_shortcut(self.inspect_current_object, context='Editor', - name='Inspect current object', parent=self) - breakpoint = create_shortcut(self.set_or_clear_breakpoint, - context='Editor', name='Breakpoint', - parent=self) - cbreakpoint = create_shortcut(self.set_or_edit_conditional_breakpoint, - context='Editor', - name='Conditional breakpoint', - parent=self) - gotoline = create_shortcut(self.go_to_line, context='Editor', - name='Go to line', parent=self) - filelist = create_shortcut(self.open_filelistdialog, context='Editor', - name='File list management', parent=self) - tab = create_shortcut(self.go_to_previous_file, context='Editor', - name='Go to previous file', parent=self) - tabshift = create_shortcut(self.go_to_next_file, context='Editor', - name='Go to next file', parent=self) - # Fixed shortcuts - new_shortcut(QKeySequence.ZoomIn, self, - lambda: self.emit(SIGNAL('zoom_in()'))) - new_shortcut("Ctrl+=", self, lambda: self.emit(SIGNAL('zoom_in()'))) - new_shortcut(QKeySequence.ZoomOut, self, - lambda: self.emit(SIGNAL('zoom_out()'))) - new_shortcut("Ctrl+0", self, lambda: self.emit(SIGNAL('zoom_reset()'))) - new_shortcut("Ctrl+W", self, self.close_file) - new_shortcut("Ctrl+F4", self, self.close_file) - # Return configurable ones - return [inspect, breakpoint, cbreakpoint, gotoline, filelist, tab, - tabshift] - - def get_shortcut_data(self): - """ - Returns shortcut data, a list of tuples (shortcut, text, default) - shortcut (QShortcut or QAction instance) - text (string): action/shortcut description - default (string): default key sequence - """ - return [sc.data for sc in self.shortcuts] - - def setup_editorstack(self, parent, layout): - """Setup editorstack's layout""" - menu_btn = create_toolbutton(self, icon=get_icon("tooloptions.png"), - tip=_("Options")) - self.menu = QMenu(self) - menu_btn.setMenu(self.menu) - menu_btn.setPopupMode(menu_btn.InstantPopup) - self.connect(self.menu, SIGNAL("aboutToShow()"), self.__setup_menu) - -# self.filelist_btn = create_toolbutton(self, -# icon=get_icon('filelist.png'), -# tip=_("File list management"), -# triggered=self.open_filelistdialog) -# -# self.previous_btn = create_toolbutton(self, -# icon=get_icon('previous.png'), -# tip=_("Previous file"), -# triggered=self.go_to_previous_file) -# -# self.next_btn = create_toolbutton(self, -# icon=get_icon('next.png'), -# tip=_("Next file"), -# triggered=self.go_to_next_file) - - # Optional tabs -# corner_widgets = {Qt.TopRightCorner: [self.previous_btn, -# self.filelist_btn, self.next_btn, -# 5, menu_btn]} - corner_widgets = {Qt.TopRightCorner: [menu_btn]} - self.tabs = BaseTabs(self, menu=self.menu, menu_use_tooltips=True, - corner_widgets=corner_widgets) - self.tabs.tabBar().setObjectName('plugin-tab') - self.tabs.set_close_function(self.close_file) - - if hasattr(self.tabs, 'setDocumentMode') \ - and not sys.platform == 'darwin': - # Don't set document mode to true on OSX because it generates - # a crash when the editor is detached from the main window - # Fixes Issue 561 - self.tabs.setDocumentMode(True) - self.connect(self.tabs, SIGNAL('currentChanged(int)'), - self.current_changed) - - if sys.platform == 'darwin': - tab_container = QWidget() - tab_container.setObjectName('tab-container') - tab_layout = QHBoxLayout(tab_container) - tab_layout.setContentsMargins(0, 0, 0, 0) - tab_layout.addWidget(self.tabs) - layout.addWidget(tab_container) - else: - layout.addWidget(self.tabs) - - def add_corner_widgets_to_tabbar(self, widgets): - self.tabs.add_corner_widgets(widgets) - - def closeEvent(self, event): - self.threadmanager.close_all_threads() - self.disconnect(self.analysis_timer, SIGNAL("timeout()"), - self.analyze_script) - QWidget.closeEvent(self, event) - if is_pyqt46: - self.emit(SIGNAL('destroyed()')) - - def clone_editor_from(self, other_finfo, set_current): - fname = other_finfo.filename - enc = other_finfo.encoding - new = other_finfo.newly_created - finfo = self.create_new_editor(fname, enc, "", - set_current=set_current, new=new, - cloned_from=other_finfo.editor) - finfo.set_analysis_results(other_finfo.analysis_results) - finfo.set_todo_results(other_finfo.todo_results) - return finfo.editor - - def clone_from(self, other): - """Clone EditorStack from other instance""" - for other_finfo in other.data: - self.clone_editor_from(other_finfo, set_current=True) - self.set_stack_index(other.get_stack_index()) - - def open_filelistdialog(self): - """Open file list management dialog box""" - self.filelist_dlg = dlg = FileListDialog(self, self.tabs, - self.fullpath_sorting_enabled) - self.connect(dlg, SIGNAL("edit_file(int)"), self.set_stack_index) - self.connect(dlg, SIGNAL("close_file(int)"), self.close_file) - dlg.synchronize(self.get_stack_index()) - dlg.exec_() - self.filelist_dlg = None - - def update_filelistdialog(self): - """Synchronize file list dialog box with editor widget tabs""" - if self.filelist_dlg is not None: - self.filelist_dlg.synchronize(self.get_stack_index()) - - def go_to_line(self): - """Go to line dialog""" - if self.data: - self.get_current_editor().exec_gotolinedialog() - - def set_or_clear_breakpoint(self): - """Set/clear breakpoint""" - if self.data: - editor = self.get_current_editor() - editor.add_remove_breakpoint() - - def set_or_edit_conditional_breakpoint(self): - """Set conditional breakpoint""" - if self.data: - editor = self.get_current_editor() - editor.add_remove_breakpoint(edit_condition=True) - - def inspect_current_object(self): - """Inspect current object in Object Inspector""" - if self.introspector: - editor = self.get_current_editor() - position = editor.get_position('cursor') - self.inspector.switch_to_editor_source() - self.introspector.show_object_info(position, auto=False) - else: - text = self.get_current_editor().get_current_object() - if text: - self.send_to_inspector(text, force=True) - - #------ Editor Widget Settings - def set_closable(self, state): - """Parent widget must handle the closable state""" - self.is_closable = state - - def set_io_actions(self, new_action, open_action, - save_action, revert_action): - self.new_action = new_action - self.open_action = open_action - self.save_action = save_action - self.revert_action = revert_action - - def set_find_widget(self, find_widget): - self.find_widget = find_widget - - def set_outlineexplorer(self, outlineexplorer): - self.outlineexplorer = outlineexplorer - self.connect(self.outlineexplorer, - SIGNAL("outlineexplorer_is_visible()"), - self._refresh_outlineexplorer) - - def initialize_outlineexplorer(self): - """This method is called separately from 'set_oulineexplorer' to avoid - doing unnecessary updates when there are multiple editor windows""" - for index in range(self.get_stack_count()): - if index != self.get_stack_index(): - self._refresh_outlineexplorer(index=index) - - def add_outlineexplorer_button(self, editor_plugin): - oe_btn = create_toolbutton(editor_plugin) - oe_btn.setDefaultAction(self.outlineexplorer.visibility_action) - self.add_corner_widgets_to_tabbar([5, oe_btn]) - - def set_inspector(self, inspector): - self.inspector = inspector - - def set_tempfile_path(self, path): - self.tempfile_path = path - - def set_title(self, text): - self.title = text - - def __update_editor_margins(self, editor): - editor.setup_margins(linenumbers=self.linenumbers_enabled, - markers=self.has_markers()) - - def __codeanalysis_settings_changed(self, current_finfo): - if self.data: - run_pyflakes, run_pep8 = self.pyflakes_enabled, self.pep8_enabled - for finfo in self.data: - self.__update_editor_margins(finfo.editor) - finfo.cleanup_analysis_results() - if (run_pyflakes or run_pep8) and current_finfo is not None: - if current_finfo is not finfo: - finfo.run_code_analysis(run_pyflakes, run_pep8) - - def set_pyflakes_enabled(self, state, current_finfo=None): - # CONF.get(self.CONF_SECTION, 'code_analysis/pyflakes') - self.pyflakes_enabled = state - self.__codeanalysis_settings_changed(current_finfo) - - def set_pep8_enabled(self, state, current_finfo=None): - # CONF.get(self.CONF_SECTION, 'code_analysis/pep8') - self.pep8_enabled = state - self.__codeanalysis_settings_changed(current_finfo) - - def has_markers(self): - """Return True if this editorstack has a marker margin for TODOs or - code analysis""" - return self.todolist_enabled or self.pyflakes_enabled\ - or self.pep8_enabled - - def set_todolist_enabled(self, state, current_finfo=None): - # CONF.get(self.CONF_SECTION, 'todo_list') - self.todolist_enabled = state - if self.data: - for finfo in self.data: - self.__update_editor_margins(finfo.editor) - finfo.cleanup_todo_results() - if state and current_finfo is not None: - if current_finfo is not finfo: - finfo.run_todo_finder() - - def set_realtime_analysis_enabled(self, state): - self.realtime_analysis_enabled = state - - def set_realtime_analysis_timeout(self, timeout): - self.analysis_timer.setInterval(timeout) - - def set_linenumbers_enabled(self, state, current_finfo=None): - # CONF.get(self.CONF_SECTION, 'line_numbers') - self.linenumbers_enabled = state - if self.data: - for finfo in self.data: - self.__update_editor_margins(finfo.editor) - - def set_blanks_enabled(self, state): - self.blanks_enabled = state - if self.data: - for finfo in self.data: - finfo.editor.set_blanks_enabled(state) - - def set_edgeline_enabled(self, state): - # CONF.get(self.CONF_SECTION, 'edge_line') - self.edgeline_enabled = state - if self.data: - for finfo in self.data: - finfo.editor.set_edge_line_enabled(state) - - def set_edgeline_column(self, column): - # CONF.get(self.CONF_SECTION, 'edge_line_column') - self.edgeline_column = column - if self.data: - for finfo in self.data: - finfo.editor.set_edge_line_column(column) - - def set_codecompletion_auto_enabled(self, state): - # CONF.get(self.CONF_SECTION, 'codecompletion_auto') - self.codecompletion_auto_enabled = state - if self.data: - for finfo in self.data: - finfo.editor.set_codecompletion_auto(state) - - def set_codecompletion_case_enabled(self, state): - self.codecompletion_case_enabled = state - if self.data: - for finfo in self.data: - finfo.editor.set_codecompletion_case(state) - - def set_codecompletion_enter_enabled(self, state): - self.codecompletion_enter_enabled = state - if self.data: - for finfo in self.data: - finfo.editor.set_codecompletion_enter(state) - - def set_calltips_enabled(self, state): - # CONF.get(self.CONF_SECTION, 'calltips') - self.calltips_enabled = state - if self.data: - for finfo in self.data: - finfo.editor.set_calltips(state) - - def set_go_to_definition_enabled(self, state): - # CONF.get(self.CONF_SECTION, 'go_to_definition') - self.go_to_definition_enabled = state - if self.data: - for finfo in self.data: - finfo.editor.set_go_to_definition_enabled(state) - - def set_close_parentheses_enabled(self, state): - # CONF.get(self.CONF_SECTION, 'close_parentheses') - self.close_parentheses_enabled = state - if self.data: - for finfo in self.data: - finfo.editor.set_close_parentheses_enabled(state) - - def set_close_quotes_enabled(self, state): - # CONF.get(self.CONF_SECTION, 'close_quotes') - self.close_quotes_enabled = state - if self.data: - for finfo in self.data: - finfo.editor.set_close_quotes_enabled(state) - - def set_add_colons_enabled(self, state): - # CONF.get(self.CONF_SECTION, 'add_colons') - self.add_colons_enabled = state - if self.data: - for finfo in self.data: - finfo.editor.set_add_colons_enabled(state) - - def set_auto_unindent_enabled(self, state): - # CONF.get(self.CONF_SECTION, 'auto_unindent') - self.auto_unindent_enabled = state - if self.data: - for finfo in self.data: - finfo.editor.set_auto_unindent_enabled(state) - - def set_indent_chars(self, indent_chars): - # CONF.get(self.CONF_SECTION, 'indent_chars') - indent_chars = indent_chars[1:-1] # removing the leading/ending '*' - self.indent_chars = indent_chars - if self.data: - for finfo in self.data: - finfo.editor.set_indent_chars(indent_chars) - - def set_tab_stop_width(self, tab_stop_width): - # CONF.get(self.CONF_SECTION, 'tab_stop_width') - self.tab_stop_width = tab_stop_width - if self.data: - for finfo in self.data: - finfo.editor.setTabStopWidth(tab_stop_width) - - def set_inspector_enabled(self, state): - self.inspector_enabled = state - - def set_default_font(self, font, color_scheme=None): - # get_font(self.CONF_SECTION) - self.default_font = font - if color_scheme is not None: - self.color_scheme = color_scheme - if self.data: - for finfo in self.data: - finfo.editor.set_font(font, color_scheme) - - def set_color_scheme(self, color_scheme): - self.color_scheme = color_scheme - if self.data: - for finfo in self.data: - finfo.editor.set_color_scheme(color_scheme) - - def set_wrap_enabled(self, state): - # CONF.get(self.CONF_SECTION, 'wrap') - self.wrap_enabled = state - if self.data: - for finfo in self.data: - finfo.editor.toggle_wrap_mode(state) - - def set_tabmode_enabled(self, state): - # CONF.get(self.CONF_SECTION, 'tab_always_indent') - self.tabmode_enabled = state - if self.data: - for finfo in self.data: - finfo.editor.set_tab_mode(state) - - def set_intelligent_backspace_enabled(self, state): - # CONF.get(self.CONF_SECTION, 'intelligent_backspace') - self.intelligent_backspace_enabled = state - if self.data: - for finfo in self.data: - finfo.editor.toggle_intelligent_backspace(state) - - def set_occurence_highlighting_enabled(self, state): - # CONF.get(self.CONF_SECTION, 'occurence_highlighting') - self.occurence_highlighting_enabled = state - if self.data: - for finfo in self.data: - finfo.editor.set_occurence_highlighting(state) - - def set_occurence_highlighting_timeout(self, timeout): - # CONF.get(self.CONF_SECTION, 'occurence_highlighting/timeout') - self.occurence_highlighting_timeout = timeout - if self.data: - for finfo in self.data: - finfo.editor.set_occurence_timeout(timeout) - - def set_highlight_current_line_enabled(self, state): - self.highlight_current_line_enabled = state - if self.data: - for finfo in self.data: - finfo.editor.set_highlight_current_line(state) - - def set_highlight_current_cell_enabled(self, state): - self.highlight_current_cell_enabled = state - if self.data: - for finfo in self.data: - finfo.editor.set_highlight_current_cell(state) - - def set_checkeolchars_enabled(self, state): - # CONF.get(self.CONF_SECTION, 'check_eol_chars') - self.checkeolchars_enabled = state - - def set_fullpath_sorting_enabled(self, state): - # CONF.get(self.CONF_SECTION, 'fullpath_sorting') - self.fullpath_sorting_enabled = state - if self.data: - finfo = self.data[self.get_stack_index()] - self.data.sort(key=self.__get_sorting_func()) - new_index = self.data.index(finfo) - self.__repopulate_stack() - self.set_stack_index(new_index) - - def set_always_remove_trailing_spaces(self, state): - # CONF.get(self.CONF_SECTION, 'always_remove_trailing_spaces') - self.always_remove_trailing_spaces = state - - def set_focus_to_editor(self, state): - self.focus_to_editor = state - - #------ Stacked widget management - def get_stack_index(self): - return self.tabs.currentIndex() - - def get_current_finfo(self): - if self.data: - return self.data[self.get_stack_index()] - - def get_current_editor(self): - return self.tabs.currentWidget() - - def get_stack_count(self): - return self.tabs.count() - - def set_stack_index(self, index): - self.tabs.setCurrentIndex(index) - - def set_tabbar_visible(self, state): - self.tabs.tabBar().setVisible(state) - - def remove_from_data(self, index): - self.tabs.blockSignals(True) - self.tabs.removeTab(index) - self.data.pop(index) - self.tabs.blockSignals(False) - self.update_actions() - self.update_filelistdialog() - - def __modified_readonly_title(self, title, is_modified, is_readonly): - if is_modified is not None and is_modified: - title += "*" - if is_readonly is not None and is_readonly: - title = "(%s)" % title - return title - - def get_tab_text(self, filename, is_modified=None, is_readonly=None): - """Return tab title""" - return self.__modified_readonly_title(osp.basename(filename), - is_modified, is_readonly) - - def get_tab_tip(self, filename, is_modified=None, is_readonly=None): - """Return tab menu title""" - if self.fullpath_sorting_enabled: - text = filename - else: - text = u("%s — %s") - text = self.__modified_readonly_title(text, - is_modified, is_readonly) - if self.tempfile_path is not None\ - and filename == encoding.to_unicode_from_fs(self.tempfile_path): - temp_file_str = to_text_string(_("Temporary file")) - if self.fullpath_sorting_enabled: - return "%s (%s)" % (text, temp_file_str) - else: - return text % (temp_file_str, self.tempfile_path) - else: - if self.fullpath_sorting_enabled: - return text - else: - return text % (osp.basename(filename), osp.dirname(filename)) - - def __get_sorting_func(self): - if self.fullpath_sorting_enabled: - return lambda item: osp.join(osp.dirname(item.filename), - '_'+osp.basename(item.filename)) - else: - return lambda item: osp.basename(item.filename) - - def add_to_data(self, finfo, set_current): - self.data.append(finfo) - self.data.sort(key=self.__get_sorting_func()) - index = self.data.index(finfo) - fname, editor = finfo.filename, finfo.editor - self.tabs.insertTab(index, editor, get_filetype_icon(fname), - self.get_tab_text(fname)) - self.set_stack_title(index, False) - if set_current: - self.set_stack_index(index) - self.current_changed(index) - self.update_actions() - self.update_filelistdialog() - - def __repopulate_stack(self): - self.tabs.blockSignals(True) - self.tabs.clear() - for finfo in self.data: - icon = get_filetype_icon(finfo.filename) - if finfo.newly_created: - is_modified = True - else: - is_modified = None - tab_text = self.get_tab_text(finfo.filename, is_modified) - tab_tip = self.get_tab_tip(finfo.filename) - index = self.tabs.addTab(finfo.editor, icon, tab_text) - self.tabs.setTabToolTip(index, tab_tip) - self.tabs.blockSignals(False) - self.update_filelistdialog() - - def rename_in_data(self, index, new_filename): - finfo = self.data[index] - if osp.splitext(finfo.filename)[1] != osp.splitext(new_filename)[1]: - # File type has changed! - txt = to_text_string(finfo.editor.get_text_with_eol()) - language = get_file_language(new_filename, txt) - finfo.editor.set_language(language) - set_new_index = index == self.get_stack_index() - current_fname = self.get_current_filename() - finfo.filename = new_filename - self.data.sort(key=self.__get_sorting_func()) - new_index = self.data.index(finfo) - self.__repopulate_stack() - if set_new_index: - self.set_stack_index(new_index) - else: - # Fixes Issue 1287 - self.set_current_filename(current_fname) - if self.outlineexplorer is not None: - self.outlineexplorer.file_renamed(finfo.editor, finfo.filename) - return new_index - - def set_stack_title(self, index, is_modified): - finfo = self.data[index] - fname = finfo.filename - is_modified = (is_modified or finfo.newly_created) and not finfo.default - is_readonly = finfo.editor.isReadOnly() - tab_text = self.get_tab_text(fname, is_modified, is_readonly) - tab_tip = self.get_tab_tip(fname, is_modified, is_readonly) - self.tabs.setTabText(index, tab_text) - self.tabs.setTabToolTip(index, tab_tip) - - - #------ Context menu - def __setup_menu(self): - """Setup tab context menu before showing it""" - self.menu.clear() - if self.data: - actions = self.menu_actions - else: - actions = (self.new_action, self.open_action) - self.setFocus() # --> Editor.__get_focus_editortabwidget - add_actions(self.menu, list(actions)+self.__get_split_actions()) - self.close_action.setEnabled(self.is_closable) - - - #------ Hor/Ver splitting - def __get_split_actions(self): - # New window - self.newwindow_action = create_action(self, _("New window"), - icon="newwindow.png", tip=_("Create a new editor window"), - triggered=lambda: self.emit(SIGNAL("create_new_window()"))) - # Splitting - self.versplit_action = create_action(self, _("Split vertically"), - icon="versplit.png", - tip=_("Split vertically this editor window"), - triggered=lambda: self.emit(SIGNAL("split_vertically()"))) - self.horsplit_action = create_action(self, _("Split horizontally"), - icon="horsplit.png", - tip=_("Split horizontally this editor window"), - triggered=lambda: self.emit(SIGNAL("split_horizontally()"))) - self.close_action = create_action(self, _("Close this panel"), - icon="close_panel.png", triggered=self.close) - return [None, self.newwindow_action, None, - self.versplit_action, self.horsplit_action, self.close_action] - - def reset_orientation(self): - self.horsplit_action.setEnabled(True) - self.versplit_action.setEnabled(True) - - def set_orientation(self, orientation): - self.horsplit_action.setEnabled(orientation == Qt.Horizontal) - self.versplit_action.setEnabled(orientation == Qt.Vertical) - - def update_actions(self): - state = self.get_stack_count() > 0 - self.horsplit_action.setEnabled(state) - self.versplit_action.setEnabled(state) - - - #------ Accessors - def get_current_filename(self): - if self.data: - return self.data[self.get_stack_index()].filename - - def has_filename(self, filename): - fixpath = lambda path: osp.normcase(osp.realpath(path)) - for index, finfo in enumerate(self.data): - if fixpath(filename) == fixpath(finfo.filename): - return index - - def set_current_filename(self, filename): - """Set current filename and return the associated editor instance""" - index = self.has_filename(filename) - if index is not None: - self.set_stack_index(index) - editor = self.data[index].editor - editor.setFocus() - return editor - - def is_file_opened(self, filename=None): - if filename is None: - # Is there any file opened? - return len(self.data) > 0 - else: - return self.has_filename(filename) - - - #------ Close file, tabwidget... - def close_file(self, index=None, force=False): - """Close file (index=None -> close current file) - Keep current file index unchanged (if current file - that is being closed)""" - current_index = self.get_stack_index() - count = self.get_stack_count() - if index is None: - if count > 0: - index = current_index - else: - self.find_widget.set_editor(None) - return - new_index = None - if count > 1: - if current_index == index: - new_index = self._get_previous_file_index() - else: - new_index = current_index - is_ok = force or self.save_if_changed(cancelable=True, index=index) - if is_ok: - finfo = self.data[index] - self.threadmanager.close_threads(finfo) - # Removing editor reference from outline explorer settings: - if self.outlineexplorer is not None: - self.outlineexplorer.remove_editor(finfo.editor) - - self.remove_from_data(index) - - # We pass self object ID as a QString, because otherwise it would - # depend on the platform: long for 64bit, int for 32bit. Replacing - # by long all the time is not working on some 32bit platforms - # (see Issue 1094, Issue 1098) - self.emit(SIGNAL('close_file(QString,int)'), str(id(self)), index) - - if not self.data and self.is_closable: - # editortabwidget is empty: removing it - # (if it's not the first editortabwidget) - self.close() - self.emit(SIGNAL('opened_files_list_changed()')) - self.emit(SIGNAL('update_code_analysis_actions()')) - self._refresh_outlineexplorer() - self.emit(SIGNAL('refresh_file_dependent_actions()')) - self.emit(SIGNAL('update_plugin_title()')) - editor = self.get_current_editor() - if editor: - editor.setFocus() - - if new_index is not None: - if index < new_index: - new_index -= 1 - self.set_stack_index(new_index) - if self.get_stack_count() == 0: - self.emit(SIGNAL('sig_new_file()')) - return False - return is_ok - - def close_all_files(self): - """Close all opened scripts""" - while self.close_file(): - pass - - - #------ Save - def save_if_changed(self, cancelable=False, index=None): - """Ask user to save file if modified""" - if index is None: - indexes = list(range(self.get_stack_count())) - else: - indexes = [index] - buttons = QMessageBox.Yes | QMessageBox.No - if cancelable: - buttons |= QMessageBox.Cancel - unsaved_nb = 0 - for index in indexes: - if self.data[index].editor.document().isModified(): - unsaved_nb += 1 - if not unsaved_nb: - # No file to save - return True - if unsaved_nb > 1: - buttons |= QMessageBox.YesAll | QMessageBox.NoAll - yes_all = False - for index in indexes: - self.set_stack_index(index) - finfo = self.data[index] - if finfo.filename == self.tempfile_path or yes_all: - if not self.save(): - return False - elif finfo.editor.document().isModified(): - answer = QMessageBox.question(self, self.title, - _("%s has been modified." - "
    Do you want to save changes?" - ) % osp.basename(finfo.filename), - buttons) - if answer == QMessageBox.Yes: - if not self.save(): - return False - elif answer == QMessageBox.YesAll: - if not self.save(): - return False - yes_all = True - elif answer == QMessageBox.NoAll: - return True - elif answer == QMessageBox.Cancel: - return False - return True - - def save(self, index=None, force=False): - """Save file""" - if index is None: - # Save the currently edited file - if not self.get_stack_count(): - return - index = self.get_stack_index() - - finfo = self.data[index] - if not finfo.editor.document().isModified() and not force: - return True - if not osp.isfile(finfo.filename) and not force: - # File has not been saved yet - return self.save_as(index=index) - if self.always_remove_trailing_spaces: - self.remove_trailing_spaces(index) - txt = to_text_string(finfo.editor.get_text_with_eol()) - try: - finfo.encoding = encoding.write(txt, finfo.filename, - finfo.encoding) - finfo.newly_created = False - self.emit(SIGNAL('encoding_changed(QString)'), finfo.encoding) - finfo.lastmodified = QFileInfo(finfo.filename).lastModified() - - # We pass self object ID as a QString, because otherwise it would - # depend on the platform: long for 64bit, int for 32bit. Replacing - # by long all the time is not working on some 32bit platforms - # (see Issue 1094, Issue 1098) - self.emit(SIGNAL('file_saved(QString,int,QString)'), - str(id(self)), index, finfo.filename) - - finfo.editor.document().setModified(False) - self.modification_changed(index=index) - self.analyze_script(index) - self.introspector.validate() - - #XXX CodeEditor-only: re-scan the whole text to rebuild outline - # explorer data from scratch (could be optimized because - # rehighlighting text means searching for all syntax coloring - # patterns instead of only searching for class/def patterns which - # would be sufficient for outline explorer data. - finfo.editor.rehighlight() - - self._refresh_outlineexplorer(index) - return True - except EnvironmentError as error: - QMessageBox.critical(self, _("Save"), - _("Unable to save script '%s'" - "

    Error message:
    %s" - ) % (osp.basename(finfo.filename), - str(error))) - return False - - def file_saved_in_other_editorstack(self, index, filename): - """ - File was just saved in another editorstack, let's synchronize! - This avoid file to be automatically reloaded - - Filename is passed in case file was just saved as another name - """ - finfo = self.data[index] - finfo.newly_created = False - finfo.filename = to_text_string(filename) - finfo.lastmodified = QFileInfo(finfo.filename).lastModified() - - def select_savename(self, original_filename): - selectedfilter = get_filter(EDIT_FILETYPES, - osp.splitext(original_filename)[1]) - self.emit(SIGNAL('redirect_stdio(bool)'), False) - filename, _selfilter = getsavefilename(self, _("Save Python script"), - original_filename, EDIT_FILTERS, selectedfilter) - self.emit(SIGNAL('redirect_stdio(bool)'), True) - if filename: - return osp.normpath(filename) - - def save_as(self, index=None): - """Save file as...""" - if index is None: - # Save the currently edited file - index = self.get_stack_index() - finfo = self.data[index] - filename = self.select_savename(finfo.filename) - if filename: - ao_index = self.has_filename(filename) - # Note: ao_index == index --> saving an untitled file - if ao_index and ao_index != index: - if not self.close_file(ao_index): - return - if ao_index < index: - index -= 1 - - new_index = self.rename_in_data(index, new_filename=filename) - - # We pass self object ID as a QString, because otherwise it would - # depend on the platform: long for 64bit, int for 32bit. Replacing - # by long all the time is not working on some 32bit platforms - # (see Issue 1094, Issue 1098) - self.emit(SIGNAL('file_renamed_in_data(QString,int,QString)'), - str(id(self)), index, filename) - - ok = self.save(index=new_index, force=True) - self.refresh(new_index) - self.set_stack_index(new_index) - return ok - else: - return False - - def save_all(self): - """Save all opened files""" - folders = set() - for index in range(self.get_stack_count()): - if self.data[index].editor.document().isModified(): - folders.add(osp.dirname(self.data[index].filename)) - self.save(index) - - #------ Update UI - def start_stop_analysis_timer(self): - self.is_analysis_done = False - if self.realtime_analysis_enabled: - self.analysis_timer.stop() - self.analysis_timer.start() - - def analyze_script(self, index=None): - """Analyze current script with pyflakes + find todos""" - if self.is_analysis_done: - return - if index is None: - index = self.get_stack_index() - if self.data: - finfo = self.data[index] - run_pyflakes, run_pep8 = self.pyflakes_enabled, self.pep8_enabled - if run_pyflakes or run_pep8: - finfo.run_code_analysis(run_pyflakes, run_pep8) - if self.todolist_enabled: - finfo.run_todo_finder() - self.is_analysis_done = True - - def set_analysis_results(self, index, analysis_results): - """Synchronize analysis results between editorstacks""" - self.data[index].set_analysis_results(analysis_results) - - def get_analysis_results(self): - if self.data: - return self.data[self.get_stack_index()].analysis_results - - def set_todo_results(self, index, todo_results): - """Synchronize todo results between editorstacks""" - self.data[index].set_todo_results(todo_results) - - def get_todo_results(self): - if self.data: - return self.data[self.get_stack_index()].todo_results - - def current_changed(self, index): - """Stack index has changed""" -# count = self.get_stack_count() -# for btn in (self.filelist_btn, self.previous_btn, self.next_btn): -# btn.setEnabled(count > 1) - - editor = self.get_current_editor() - if index != -1: - editor.setFocus() - if DEBUG_EDITOR: - print("setfocusto:", editor, file=STDOUT) - else: - self.emit(SIGNAL('reset_statusbar()')) - self.emit(SIGNAL('opened_files_list_changed()')) - - # Index history management - id_list = [id(self.tabs.widget(_i)) - for _i in range(self.tabs.count())] - for _id in self.stack_history[:]: - if _id not in id_list: - self.stack_history.pop(self.stack_history.index(_id)) - current_id = id(self.tabs.widget(index)) - while current_id in self.stack_history: - self.stack_history.pop(self.stack_history.index(current_id)) - self.stack_history.append(current_id) - if DEBUG_EDITOR: - print("current_changed:", index, self.data[index].editor, end=' ', file=STDOUT) - print(self.data[index].editor.get_document_id(), file=STDOUT) - - self.emit(SIGNAL('update_plugin_title()')) - if editor is not None: - self.emit(SIGNAL('current_file_changed(QString,int)'), - self.data[index].filename, editor.get_position('cursor')) - - def _get_previous_file_index(self): - if len(self.stack_history) > 1: - last = len(self.stack_history)-1 - w_id = self.stack_history.pop(last) - self.stack_history.insert(0, w_id) - last_id = self.stack_history[last] - for _i in range(self.tabs.count()): - if id(self.tabs.widget(_i)) == last_id: - return _i - - def go_to_previous_file(self): - """Ctrl+Tab""" - prev_index = self._get_previous_file_index() - if prev_index is not None: - self.set_stack_index(prev_index) - elif len(self.stack_history) == 0 and self.get_stack_count(): - self.stack_history = [id(self.tabs.currentWidget())] - - def go_to_next_file(self): - """Ctrl+Shift+Tab""" - if len(self.stack_history) > 1: - last = len(self.stack_history)-1 - w_id = self.stack_history.pop(0) - self.stack_history.append(w_id) - last_id = self.stack_history[last] - for _i in range(self.tabs.count()): - if id(self.tabs.widget(_i)) == last_id: - self.set_stack_index(_i) - break - elif len(self.stack_history) == 0 and self.get_stack_count(): - self.stack_history = [id(self.tabs.currentWidget())] - - def focus_changed(self): - """Editor focus has changed""" - fwidget = QApplication.focusWidget() - for finfo in self.data: - if fwidget is finfo.editor: - self.refresh() - self.emit(SIGNAL("editor_focus_changed()")) - - def _refresh_outlineexplorer(self, index=None, update=True, clear=False): - """Refresh outline explorer panel""" - oe = self.outlineexplorer - if oe is None: - return - if index is None: - index = self.get_stack_index() - enable = False - if self.data: - finfo = self.data[index] - if finfo.editor.is_python(): - enable = True - oe.setEnabled(True) - oe.set_current_editor(finfo.editor, finfo.filename, - update=update, clear=clear) - if not enable: - oe.setEnabled(False) - - def __refresh_statusbar(self, index): - """Refreshing statusbar widgets""" - finfo = self.data[index] - self.emit(SIGNAL('encoding_changed(QString)'), finfo.encoding) - # Refresh cursor position status: - line, index = finfo.editor.get_cursor_line_column() - self.emit(SIGNAL('editor_cursor_position_changed(int,int)'), - line, index) - - def __refresh_readonly(self, index): - finfo = self.data[index] - read_only = not QFileInfo(finfo.filename).isWritable() - if not osp.isfile(finfo.filename): - # This is an 'untitledX.py' file (newly created) - read_only = False - finfo.editor.setReadOnly(read_only) - self.emit(SIGNAL('readonly_changed(bool)'), read_only) - - def __check_file_status(self, index): - """Check if file has been changed in any way outside Spyder: - 1. removed, moved or renamed outside Spyder - 2. modified outside Spyder""" - if self.__file_status_flag: - # Avoid infinite loop: when the QMessageBox.question pops, it - # gets focus and then give it back to the CodeEditor instance, - # triggering a refresh cycle which calls this method - return - self.__file_status_flag = True - - finfo = self.data[index] - name = osp.basename(finfo.filename) - - if finfo.newly_created: - # File was just created (not yet saved): do nothing - # (do not return because of the clean-up at the end of the method) - pass - - elif not osp.isfile(finfo.filename): - # File doesn't exist (removed, moved or offline): - answer = QMessageBox.warning(self, self.title, - _("%s is unavailable " - "(this file may have been removed, moved " - "or renamed outside Spyder)." - "
    Do you want to close it?") % name, - QMessageBox.Yes | QMessageBox.No) - if answer == QMessageBox.Yes: - self.close_file(index) - else: - finfo.newly_created = True - finfo.editor.document().setModified(True) - self.modification_changed(index=index) - - else: - # Else, testing if it has been modified elsewhere: - lastm = QFileInfo(finfo.filename).lastModified() - if to_text_string(lastm.toString()) \ - != to_text_string(finfo.lastmodified.toString()): - if finfo.editor.document().isModified(): - answer = QMessageBox.question(self, - self.title, - _("%s has been modified outside Spyder." - "
    Do you want to reload it and lose all " - "your changes?") % name, - QMessageBox.Yes | QMessageBox.No) - if answer == QMessageBox.Yes: - self.reload(index) - else: - finfo.lastmodified = lastm - else: - self.reload(index) - - # Finally, resetting temporary flag: - self.__file_status_flag = False - - def refresh(self, index=None): - """Refresh tabwidget""" - if index is None: - index = self.get_stack_index() - # Set current editor - if self.get_stack_count(): - index = self.get_stack_index() - finfo = self.data[index] - editor = finfo.editor - editor.setFocus() - self._refresh_outlineexplorer(index, update=False) - self.emit(SIGNAL('update_code_analysis_actions()')) - self.__refresh_statusbar(index) - self.__refresh_readonly(index) - self.__check_file_status(index) - self.emit(SIGNAL('update_plugin_title()')) - else: - editor = None - # Update the modification-state-dependent parameters - self.modification_changed() - # Update FindReplace binding - self.find_widget.set_editor(editor, refresh=False) - - def modification_changed(self, state=None, index=None, editor_id=None): - """ - Current editor's modification state has changed - --> change tab title depending on new modification state - --> enable/disable save/save all actions - """ - if editor_id is not None: - for index, _finfo in enumerate(self.data): - if id(_finfo.editor) == editor_id: - break - # This must be done before refreshing save/save all actions: - # (otherwise Save/Save all actions will always be enabled) - self.emit(SIGNAL('opened_files_list_changed()')) - # -- - if index is None: - index = self.get_stack_index() - if index == -1: - return - finfo = self.data[index] - if state is None: - state = finfo.editor.document().isModified() - self.set_stack_title(index, state) - # Toggle save/save all actions state - self.save_action.setEnabled(state) - self.emit(SIGNAL('refresh_save_all_action()')) - # Refreshing eol mode - eol_chars = finfo.editor.get_line_separator() - os_name = sourcecode.get_os_name_from_eol_chars(eol_chars) - self.emit(SIGNAL('refresh_eol_chars(QString)'), os_name) - - - #------ Load, reload - def reload(self, index): - """Reload file from disk""" - finfo = self.data[index] - txt, finfo.encoding = encoding.read(finfo.filename) - finfo.lastmodified = QFileInfo(finfo.filename).lastModified() - position = finfo.editor.get_position('cursor') - finfo.editor.set_text(txt) - finfo.editor.document().setModified(False) - finfo.editor.set_cursor_position(position) - self.introspector.validate() - - #XXX CodeEditor-only: re-scan the whole text to rebuild outline - # explorer data from scratch (could be optimized because - # rehighlighting text means searching for all syntax coloring - # patterns instead of only searching for class/def patterns which - # would be sufficient for outline explorer data. - finfo.editor.rehighlight() - - self._refresh_outlineexplorer(index) - - def revert(self): - """Revert file from disk""" - index = self.get_stack_index() - finfo = self.data[index] - filename = finfo.filename - if finfo.editor.document().isModified(): - answer = QMessageBox.warning(self, self.title, - _("All changes to %s will be lost." - "
    Do you want to revert file from disk?" - ) % osp.basename(filename), - QMessageBox.Yes|QMessageBox.No) - if answer != QMessageBox.Yes: - return - self.reload(index) - - def create_new_editor(self, fname, enc, txt, set_current, new=False, - cloned_from=None): - """ - Create a new editor instance - Returns finfo object (instead of editor as in previous releases) - """ - editor = codeeditor.CodeEditor(self) - introspector = self.introspector - self.connect(editor, SIGNAL("get_completions(bool)"), - introspector.get_completions) - self.connect(editor, SIGNAL("show_object_info(int)"), - introspector.show_object_info) - self.connect(editor, SIGNAL("go_to_definition(int)"), - introspector.go_to_definition) - - finfo = FileInfo(fname, enc, editor, new, self.threadmanager, - self.introspector) - self.add_to_data(finfo, set_current) - self.connect(finfo, SIGNAL( - "send_to_inspector(QString,QString,QString,QString,bool)"), - self.send_to_inspector) - self.connect(finfo, SIGNAL('analysis_results_changed()'), - lambda: self.emit(SIGNAL('analysis_results_changed()'))) - self.connect(finfo, SIGNAL('todo_results_changed()'), - lambda: self.emit(SIGNAL('todo_results_changed()'))) - self.connect(finfo, SIGNAL("edit_goto(QString,int,QString)"), - lambda fname, lineno, name: - self.emit(SIGNAL("edit_goto(QString,int,QString)"), - fname, lineno, name)) - self.connect(finfo, SIGNAL("save_breakpoints(QString,QString)"), - lambda s1, s2: - self.emit(SIGNAL("save_breakpoints(QString,QString)"), - s1, s2)) - self.connect(editor, SIGNAL("run_selection()"), self.run_selection) - self.connect(editor, SIGNAL("run_cell()"), self.run_cell) - self.connect(editor, SIGNAL('run_cell_and_advance()'), - self.run_cell_and_advance) - editor.sig_new_file.connect(lambda s: self.parent().plugin.new(text=s)) - language = get_file_language(fname, txt) - editor.setup_editor( - linenumbers=self.linenumbers_enabled, - show_blanks=self.blanks_enabled, - edge_line=self.edgeline_enabled, - edge_line_column=self.edgeline_column, language=language, - markers=self.has_markers(), font=self.default_font, - color_scheme=self.color_scheme, - wrap=self.wrap_enabled, tab_mode=self.tabmode_enabled, - intelligent_backspace=self.intelligent_backspace_enabled, - highlight_current_line=self.highlight_current_line_enabled, - highlight_current_cell=self.highlight_current_cell_enabled, - occurence_highlighting=self.occurence_highlighting_enabled, - occurence_timeout=self.occurence_highlighting_timeout, - codecompletion_auto=self.codecompletion_auto_enabled, - codecompletion_case=self.codecompletion_case_enabled, - codecompletion_enter=self.codecompletion_enter_enabled, - calltips=self.calltips_enabled, - go_to_definition=self.go_to_definition_enabled, - close_parentheses=self.close_parentheses_enabled, - close_quotes=self.close_quotes_enabled, - add_colons=self.add_colons_enabled, - auto_unindent=self.auto_unindent_enabled, - indent_chars=self.indent_chars, - tab_stop_width=self.tab_stop_width, - cloned_from=cloned_from) - if cloned_from is None: - editor.set_text(txt) - editor.document().setModified(False) - self.connect(finfo, SIGNAL('text_changed_at(QString,int)'), - lambda fname, position: - self.emit(SIGNAL('text_changed_at(QString,int)'), - fname, position)) - self.connect(editor, SIGNAL('cursorPositionChanged(int,int)'), - self.editor_cursor_position_changed) - self.connect(editor, SIGNAL('textChanged()'), - self.start_stop_analysis_timer) - self.connect(editor, SIGNAL('modificationChanged(bool)'), - lambda state: self.modification_changed(state, - editor_id=id(editor))) - self.connect(editor, SIGNAL("focus_in()"), self.focus_changed) - self.connect(editor, SIGNAL('zoom_in()'), - lambda: self.emit(SIGNAL('zoom_in()'))) - self.connect(editor, SIGNAL('zoom_out()'), - lambda: self.emit(SIGNAL('zoom_out()'))) - self.connect(editor, SIGNAL('zoom_reset()'), - lambda: self.emit(SIGNAL('zoom_reset()'))) - if self.outlineexplorer is not None: - # Removing editor reference from outline explorer settings: - self.connect(editor, SIGNAL("destroyed()"), - lambda obj=editor: - self.outlineexplorer.remove_editor(obj)) - - self.find_widget.set_editor(editor) - - self.emit(SIGNAL('refresh_file_dependent_actions()')) - self.modification_changed(index=self.data.index(finfo)) - - return finfo - - def editor_cursor_position_changed(self, line, index): - """Cursor position of one of the editor in the stack has changed""" - self.emit(SIGNAL('editor_cursor_position_changed(int,int)'), - line, index) - - def send_to_inspector(self, qstr1, qstr2=None, qstr3=None, - qstr4=None, force=False): - """qstr1: obj_text, qstr2: argpspec, qstr3: note, qstr4: doc_text""" - if not force and not self.inspector_enabled: - return - if self.inspector is not None \ - and (force or self.inspector.dockwidget.isVisible()): - # ObjectInspector widget exists and is visible - if qstr4 is None: - self.inspector.set_object_text(qstr1, ignore_unknown=True, - force_refresh=force) - else: - objtxt = to_text_string(qstr1) - name = objtxt.split('.')[-1] - argspec = to_text_string(qstr2) - note = to_text_string(qstr3) - docstring = to_text_string(qstr4) - doc = {'obj_text': objtxt, 'name': name, 'argspec': argspec, - 'note': note, 'docstring': docstring} - self.inspector.set_editor_doc(doc, force_refresh=force) - editor = self.get_current_editor() - editor.setFocus() - - def new(self, filename, encoding, text, default_content=False): - """ - Create new filename with *encoding* and *text* - """ - finfo = self.create_new_editor(filename, encoding, text, - set_current=False, new=True) - finfo.editor.set_cursor_position('eof') - finfo.editor.insert_text(os.linesep) - if default_content: - finfo.default = True - finfo.editor.document().setModified(False) - return finfo - - def load(self, filename, set_current=True): - """ - Load filename, create an editor instance and return it - *Warning* This is loading file, creating editor but not executing - the source code analysis -- the analysis must be done by the editor - plugin (in case multiple editorstack instances are handled) - """ - filename = osp.abspath(to_text_string(filename)) - self.emit(SIGNAL('starting_long_process(QString)'), - _("Loading %s...") % filename) - text, enc = encoding.read(filename) - finfo = self.create_new_editor(filename, enc, text, set_current) - index = self.data.index(finfo) - self._refresh_outlineexplorer(index, update=True) - self.emit(SIGNAL('ending_long_process(QString)'), "") - if self.isVisible() and self.checkeolchars_enabled \ - and sourcecode.has_mixed_eol_chars(text): - name = osp.basename(filename) - QMessageBox.warning(self, self.title, - _("%s contains mixed end-of-line " - "characters.
    Spyder will fix this " - "automatically.") % name, - QMessageBox.Ok) - self.set_os_eol_chars(index) - self.is_analysis_done = False - return finfo - - def set_os_eol_chars(self, index=None): - if index is None: - index = self.get_stack_index() - finfo = self.data[index] - eol_chars = sourcecode.get_eol_chars_from_os_name(os.name) - finfo.editor.set_eol_chars(eol_chars) - finfo.editor.document().setModified(True) - - def remove_trailing_spaces(self, index=None): - """Remove trailing spaces""" - if index is None: - index = self.get_stack_index() - finfo = self.data[index] - finfo.editor.remove_trailing_spaces() - - def fix_indentation(self, index=None): - """Replace tab characters by spaces""" - if index is None: - index = self.get_stack_index() - finfo = self.data[index] - finfo.editor.fix_indentation() - - #------ Run - def run_selection(self): - """Run selected text or current line in console""" - text = self.get_current_editor().get_selection_as_executable_code() - if not text: - line = self.get_current_editor().get_current_line() - text = line.lstrip() - self.emit(SIGNAL('exec_in_extconsole(QString,bool)'), text, - self.focus_to_editor) - - def run_cell(self): - """Run current cell""" - text = self.get_current_editor().get_cell_as_executable_code() - finfo = self.get_current_finfo() - if finfo.editor.is_python() and text: - self.emit(SIGNAL('exec_in_extconsole(QString,bool)'), - text, self.focus_to_editor) - - def run_cell_and_advance(self): - """Run current cell and advance to the next one""" - self.run_cell() - if self.focus_to_editor: - self.get_current_editor().go_to_next_cell() - else: - term = QApplication.focusWidget() - self.get_current_editor().go_to_next_cell() - term.setFocus() - - #------ Drag and drop - def dragEnterEvent(self, event): - """Reimplement Qt method - Inform Qt about the types of data that the widget accepts""" - source = event.mimeData() - # The second check is necessary on Windows, where source.hasUrls() - # can return True but source.urls() is [] - if source.hasUrls() and source.urls(): - if mimedata2url(source, extlist=EDIT_EXT): - event.acceptProposedAction() - else: - all_urls = mimedata2url(source) - text = [encoding.is_text_file(url) for url in all_urls] - if any(text): - event.acceptProposedAction() - else: - event.ignore() - elif source.hasText(): - event.acceptProposedAction() - elif os.name == 'nt': - # This covers cases like dragging from compressed files, - # which can be opened by the Editor if they are plain - # text, but doesn't come with url info. - # Fixes Issue 2032 - event.acceptProposedAction() - else: - event.ignore() - - def dropEvent(self, event): - """Reimplement Qt method - Unpack dropped data and handle it""" - source = event.mimeData() - if source.hasUrls(): - files = mimedata2url(source) - files = [f for f in files if encoding.is_text_file(f)] - supported_files = mimedata2url(source, extlist=EDIT_EXT) - files = set(files or []) | set(supported_files or []) - for fname in files: - self.emit(SIGNAL('plugin_load(QString)'), fname) - elif source.hasText(): - editor = self.get_current_editor() - if editor is not None: - editor.insert_text( source.text() ) - event.acceptProposedAction() - - -class EditorSplitter(QSplitter): - def __init__(self, parent, plugin, menu_actions, first=False, - register_editorstack_cb=None, unregister_editorstack_cb=None): - QSplitter.__init__(self, parent) - self.setAttribute(Qt.WA_DeleteOnClose) - self.setChildrenCollapsible(False) - - self.toolbar_list = None - self.menu_list = None - - self.plugin = plugin - - if register_editorstack_cb is None: - register_editorstack_cb = self.plugin.register_editorstack - self.register_editorstack_cb = register_editorstack_cb - if unregister_editorstack_cb is None: - unregister_editorstack_cb = self.plugin.unregister_editorstack - self.unregister_editorstack_cb = unregister_editorstack_cb - - self.menu_actions = menu_actions - self.editorstack = EditorStack(self, menu_actions) - self.register_editorstack_cb(self.editorstack) - if not first: - self.plugin.clone_editorstack(editorstack=self.editorstack) - self.connect(self.editorstack, SIGNAL("destroyed()"), - lambda: self.editorstack_closed()) - self.connect(self.editorstack, SIGNAL("split_vertically()"), - lambda: self.split(orientation=Qt.Vertical)) - self.connect(self.editorstack, SIGNAL("split_horizontally()"), - lambda: self.split(orientation=Qt.Horizontal)) - self.addWidget(self.editorstack) - - def closeEvent(self, event): - QSplitter.closeEvent(self, event) - if is_pyqt46: - self.emit(SIGNAL('destroyed()')) - - def __give_focus_to_remaining_editor(self): - focus_widget = self.plugin.get_focus_widget() - if focus_widget is not None: - focus_widget.setFocus() - - def editorstack_closed(self): - if DEBUG_EDITOR: - print("method 'editorstack_closed':", file=STDOUT) - print(" self :", self, file=STDOUT) -# print >>STDOUT, " sender:", self.sender() - self.unregister_editorstack_cb(self.editorstack) - self.editorstack = None - try: - close_splitter = self.count() == 1 - except RuntimeError: - # editorsplitter has been destroyed (happens when closing a - # EditorMainWindow instance) - return - if close_splitter: - # editorstack just closed was the last widget in this QSplitter - self.close() - return - self.__give_focus_to_remaining_editor() - - def editorsplitter_closed(self): - if DEBUG_EDITOR: - print("method 'editorsplitter_closed':", file=STDOUT) - print(" self :", self, file=STDOUT) -# print >>STDOUT, " sender:", self.sender() - try: - close_splitter = self.count() == 1 and self.editorstack is None - except RuntimeError: - # editorsplitter has been destroyed (happens when closing a - # EditorMainWindow instance) - return - if close_splitter: - # editorsplitter just closed was the last widget in this QSplitter - self.close() - return - elif self.count() == 2 and self.editorstack: - # back to the initial state: a single editorstack instance, - # as a single widget in this QSplitter: orientation may be changed - self.editorstack.reset_orientation() - self.__give_focus_to_remaining_editor() - - def split(self, orientation=Qt.Vertical): - self.setOrientation(orientation) - self.editorstack.set_orientation(orientation) - editorsplitter = EditorSplitter(self.parent(), self.plugin, - self.menu_actions, - register_editorstack_cb=self.register_editorstack_cb, - unregister_editorstack_cb=self.unregister_editorstack_cb) - self.addWidget(editorsplitter) - self.connect(editorsplitter, SIGNAL("destroyed()"), - lambda: self.editorsplitter_closed()) - current_editor = editorsplitter.editorstack.get_current_editor() - if current_editor is not None: - current_editor.setFocus() - - def iter_editorstacks(self): - editorstacks = [(self.widget(0), self.orientation())] - if self.count() > 1: - editorsplitter = self.widget(1) - editorstacks += editorsplitter.iter_editorstacks() - return editorstacks - - def get_layout_settings(self): - """Return layout state""" - splitsettings = [] - for editorstack, orientation in self.iter_editorstacks(): - clines = [finfo.editor.get_cursor_line_number() - for finfo in editorstack.data] - cfname = editorstack.get_current_filename() - splitsettings.append((orientation == Qt.Vertical, cfname, clines)) - return dict(hexstate=qbytearray_to_str(self.saveState()), - sizes=self.sizes(), splitsettings=splitsettings) - - def set_layout_settings(self, settings): - """Restore layout state""" - splitsettings = settings.get('splitsettings') - if splitsettings is None: - return - splitter = self - editor = None - for index, (is_vertical, cfname, clines) in enumerate(splitsettings): - if index > 0: - splitter.split(Qt.Vertical if is_vertical else Qt.Horizontal) - splitter = splitter.widget(1) - editorstack = splitter.widget(0) - for index, finfo in enumerate(editorstack.data): - editor = finfo.editor - editor.go_to_line(clines[index]) - editorstack.set_current_filename(cfname) - hexstate = settings.get('hexstate') - if hexstate is not None: - self.restoreState( QByteArray().fromHex(str(hexstate)) ) - sizes = settings.get('sizes') - if sizes is not None: - self.setSizes(sizes) - if editor is not None: - editor.clearFocus() - editor.setFocus() - - -class EditorWidget(QSplitter): - def __init__(self, parent, plugin, menu_actions, show_fullpath, - fullpath_sorting, show_all_files, show_comments): - QSplitter.__init__(self, parent) - self.setAttribute(Qt.WA_DeleteOnClose) - - statusbar = parent.statusBar() # Create a status bar - self.readwrite_status = ReadWriteStatus(self, statusbar) - self.eol_status = EOLStatus(self, statusbar) - self.encoding_status = EncodingStatus(self, statusbar) - self.cursorpos_status = CursorPositionStatus(self, statusbar) - - self.editorstacks = [] - - self.plugin = plugin - - self.find_widget = FindReplace(self, enable_replace=True) - self.plugin.register_widget_shortcuts("Editor", self.find_widget) - self.find_widget.hide() - self.outlineexplorer = OutlineExplorerWidget(self, - show_fullpath=show_fullpath, - fullpath_sorting=fullpath_sorting, - show_all_files=show_all_files, - show_comments=show_comments) - self.connect(self.outlineexplorer, - SIGNAL("edit_goto(QString,int,QString)"), - lambda filenames, goto, word: - plugin.load(filenames=filenames, goto=goto, word=word, - editorwindow=self.parent())) - - editor_widgets = QWidget(self) - editor_layout = QVBoxLayout() - editor_layout.setContentsMargins(0, 0, 0, 0) - editor_widgets.setLayout(editor_layout) - editorsplitter = EditorSplitter(self, plugin, menu_actions, - register_editorstack_cb=self.register_editorstack, - unregister_editorstack_cb=self.unregister_editorstack) - self.editorsplitter = editorsplitter - editor_layout.addWidget(editorsplitter) - editor_layout.addWidget(self.find_widget) - - splitter = QSplitter(self) - splitter.setContentsMargins(0, 0, 0, 0) - splitter.addWidget(editor_widgets) - splitter.addWidget(self.outlineexplorer) - splitter.setStretchFactor(0, 5) - splitter.setStretchFactor(1, 1) - - # Refreshing outline explorer - editorsplitter.editorstack.initialize_outlineexplorer() - - def register_editorstack(self, editorstack): - self.editorstacks.append(editorstack) - if DEBUG_EDITOR: - print("EditorWidget.register_editorstack:", editorstack, file=STDOUT) - self.__print_editorstacks() - self.plugin.last_focus_editorstack[self.parent()] = editorstack - editorstack.set_closable( len(self.editorstacks) > 1 ) - editorstack.set_outlineexplorer(self.outlineexplorer) - editorstack.set_find_widget(self.find_widget) - self.connect(editorstack, SIGNAL('reset_statusbar()'), - self.readwrite_status.hide) - self.connect(editorstack, SIGNAL('reset_statusbar()'), - self.encoding_status.hide) - self.connect(editorstack, SIGNAL('reset_statusbar()'), - self.cursorpos_status.hide) - self.connect(editorstack, SIGNAL('readonly_changed(bool)'), - self.readwrite_status.readonly_changed) - self.connect(editorstack, SIGNAL('encoding_changed(QString)'), - self.encoding_status.encoding_changed) - self.connect(editorstack, - SIGNAL('editor_cursor_position_changed(int,int)'), - self.cursorpos_status.cursor_position_changed) - self.connect(editorstack, SIGNAL('refresh_eol_chars(QString)'), - self.eol_status.eol_changed) - self.plugin.register_editorstack(editorstack) - oe_btn = create_toolbutton(self) - oe_btn.setDefaultAction(self.outlineexplorer.visibility_action) - editorstack.add_corner_widgets_to_tabbar([5, oe_btn]) - - def __print_editorstacks(self): - print("%d editorstack(s) in editorwidget:" \ - % len(self.editorstacks), file=STDOUT) - for edst in self.editorstacks: - print(" ", edst, file=STDOUT) - - def unregister_editorstack(self, editorstack): - if DEBUG_EDITOR: - print("EditorWidget.unregister_editorstack:", editorstack, file=STDOUT) - self.plugin.unregister_editorstack(editorstack) - self.editorstacks.pop(self.editorstacks.index(editorstack)) - if DEBUG_EDITOR: - self.__print_editorstacks() - - -class EditorMainWindow(QMainWindow): - def __init__(self, plugin, menu_actions, toolbar_list, menu_list, - show_fullpath, fullpath_sorting, show_all_files, - show_comments): - QMainWindow.__init__(self) - self.setAttribute(Qt.WA_DeleteOnClose) - - self.window_size = None - - self.editorwidget = EditorWidget(self, plugin, menu_actions, - show_fullpath, fullpath_sorting, - show_all_files, show_comments) - self.setCentralWidget(self.editorwidget) - - # Give focus to current editor to update/show all status bar widgets - editorstack = self.editorwidget.editorsplitter.editorstack - editor = editorstack.get_current_editor() - if editor is not None: - editor.setFocus() - - self.setWindowTitle("Spyder - %s" % plugin.windowTitle()) - self.setWindowIcon(plugin.windowIcon()) - - if toolbar_list: - toolbars = [] - for title, actions in toolbar_list: - toolbar = self.addToolBar(title) - toolbar.setObjectName(str(id(toolbar))) - add_actions(toolbar, actions) - toolbars.append(toolbar) - if menu_list: - quit_action = create_action(self, _("Close window"), - icon="close_panel.png", - tip=_("Close this window"), - triggered=self.close) - menus = [] - for index, (title, actions) in enumerate(menu_list): - menu = self.menuBar().addMenu(title) - if index == 0: - # File menu - add_actions(menu, actions+[None, quit_action]) - else: - add_actions(menu, actions) - menus.append(menu) - - def resizeEvent(self, event): - """Reimplement Qt method""" - if not self.isMaximized() and not self.isFullScreen(): - self.window_size = self.size() - QMainWindow.resizeEvent(self, event) - - def closeEvent(self, event): - """Reimplement Qt method""" - QMainWindow.closeEvent(self, event) - if is_pyqt46: - self.emit(SIGNAL('destroyed()')) - for editorstack in self.editorwidget.editorstacks[:]: - if DEBUG_EDITOR: - print("--> destroy_editorstack:", editorstack, file=STDOUT) - editorstack.emit(SIGNAL('destroyed()')) - - def get_layout_settings(self): - """Return layout state""" - splitsettings = self.editorwidget.editorsplitter.get_layout_settings() - return dict(size=(self.window_size.width(), self.window_size.height()), - pos=(self.pos().x(), self.pos().y()), - is_maximized=self.isMaximized(), - is_fullscreen=self.isFullScreen(), - hexstate=qbytearray_to_str(self.saveState()), - splitsettings=splitsettings) - - def set_layout_settings(self, settings): - """Restore layout state""" - size = settings.get('size') - if size is not None: - self.resize( QSize(*size) ) - self.window_size = self.size() - pos = settings.get('pos') - if pos is not None: - self.move( QPoint(*pos) ) - hexstate = settings.get('hexstate') - if hexstate is not None: - self.restoreState( QByteArray().fromHex(str(hexstate)) ) - if settings.get('is_maximized'): - self.setWindowState(Qt.WindowMaximized) - if settings.get('is_fullscreen'): - self.setWindowState(Qt.WindowFullScreen) - splitsettings = settings.get('splitsettings') - if splitsettings is not None: - self.editorwidget.editorsplitter.set_layout_settings(splitsettings) - - -class EditorPluginExample(QSplitter): - def __init__(self): - QSplitter.__init__(self) - - menu_actions = [] - - self.editorstacks = [] - self.editorwindows = [] - - self.last_focus_editorstack = {} # fake - - self.find_widget = FindReplace(self, enable_replace=True) - self.outlineexplorer = OutlineExplorerWidget(self, show_fullpath=False, - show_all_files=False) - self.connect(self.outlineexplorer, - SIGNAL("edit_goto(QString,int,QString)"), - self.go_to_file) - - editor_widgets = QWidget(self) - editor_layout = QVBoxLayout() - editor_layout.setContentsMargins(0, 0, 0, 0) - editor_widgets.setLayout(editor_layout) - editor_layout.addWidget(EditorSplitter(self, self, menu_actions, - first=True)) - editor_layout.addWidget(self.find_widget) - - self.setContentsMargins(0, 0, 0, 0) - self.addWidget(editor_widgets) - self.addWidget(self.outlineexplorer) - - self.setStretchFactor(0, 5) - self.setStretchFactor(1, 1) - - self.menu_actions = menu_actions - self.toolbar_list = None - self.menu_list = None - self.setup_window([], []) - - def go_to_file(self, fname, lineno, text): - editorstack = self.editorstacks[0] - editorstack.set_current_filename(to_text_string(fname)) - editor = editorstack.get_current_editor() - editor.go_to_line(lineno, word=text) - - def closeEvent(self, event): - for win in self.editorwindows[:]: - win.close() - if DEBUG_EDITOR: - print(len(self.editorwindows), ":", self.editorwindows, file=STDOUT) - print(len(self.editorstacks), ":", self.editorstacks, file=STDOUT) - - event.accept() - - def load(self, fname): - QApplication.processEvents() - editorstack = self.editorstacks[0] - editorstack.load(fname) - editorstack.analyze_script() - - def register_editorstack(self, editorstack): - if DEBUG_EDITOR: - print("FakePlugin.register_editorstack:", editorstack, file=STDOUT) - self.editorstacks.append(editorstack) - if self.isAncestorOf(editorstack): - # editorstack is a child of the Editor plugin - editorstack.set_fullpath_sorting_enabled(True) - editorstack.set_closable( len(self.editorstacks) > 1 ) - editorstack.set_outlineexplorer(self.outlineexplorer) - editorstack.set_find_widget(self.find_widget) - oe_btn = create_toolbutton(self) - oe_btn.setDefaultAction(self.outlineexplorer.visibility_action) - editorstack.add_corner_widgets_to_tabbar([5, oe_btn]) - - action = QAction(self) - editorstack.set_io_actions(action, action, action, action) - font = QFont("Courier New") - font.setPointSize(10) - editorstack.set_default_font(font, color_scheme='Spyder') - - self.connect(editorstack, SIGNAL('close_file(QString,int)'), - self.close_file_in_all_editorstacks) - self.connect(editorstack, SIGNAL('file_saved(QString,int,QString)'), - self.file_saved_in_editorstack) - self.connect(editorstack, - SIGNAL('file_renamed_in_data(QString,int,QString)'), - self.file_renamed_in_data_in_editorstack) - - self.connect(editorstack, SIGNAL("create_new_window()"), - self.create_new_window) - self.connect(editorstack, SIGNAL('plugin_load(QString)'), - self.load) - - def unregister_editorstack(self, editorstack): - if DEBUG_EDITOR: - print("FakePlugin.unregister_editorstack:", editorstack, file=STDOUT) - self.editorstacks.pop(self.editorstacks.index(editorstack)) - - def clone_editorstack(self, editorstack): - editorstack.clone_from(self.editorstacks[0]) - - def setup_window(self, toolbar_list, menu_list): - self.toolbar_list = toolbar_list - self.menu_list = menu_list - - def create_new_window(self): - window = EditorMainWindow(self, self.menu_actions, - self.toolbar_list, self.menu_list, - show_fullpath=False, fullpath_sorting=True, - show_all_files=False, show_comments=True) - window.resize(self.size()) - window.show() - self.register_editorwindow(window) - self.connect(window, SIGNAL("destroyed()"), - lambda win=window: self.unregister_editorwindow(win)) - - def register_editorwindow(self, window): - if DEBUG_EDITOR: - print("register_editorwindowQObject*:", window, file=STDOUT) - self.editorwindows.append(window) - - def unregister_editorwindow(self, window): - if DEBUG_EDITOR: - print("unregister_editorwindow:", window, file=STDOUT) - self.editorwindows.pop(self.editorwindows.index(window)) - - def get_focus_widget(self): - pass - - @Slot(int, int) - def close_file_in_all_editorstacks(self, editorstack_id_str, index): - for editorstack in self.editorstacks: - if str(id(editorstack)) != editorstack_id_str: - editorstack.blockSignals(True) - editorstack.close_file(index, force=True) - editorstack.blockSignals(False) - - # This method is never called in this plugin example. It's here only - # to show how to use the file_saved signal (see above). - @Slot(int, int) - def file_saved_in_editorstack(self, editorstack_id_str, index, filename): - """A file was saved in editorstack, this notifies others""" - for editorstack in self.editorstacks: - if str(id(editorstack)) != editorstack_id_str: - editorstack.file_saved_in_other_editorstack(index, filename) - - # This method is never called in this plugin example. It's here only - # to show how to use the file_saved signal (see above). - @Slot(int, int) - def file_renamed_in_data_in_editorstack(self, editorstack_id_str, - index, filename): - """A file was renamed in data in editorstack, this notifies others""" - for editorstack in self.editorstacks: - if str(id(editorstack)) != editorstack_id_str: - editorstack.rename_in_data(index, filename) - - def register_widget_shortcuts(self, context, widget): - """Fake!""" - pass - -def test(): - from spyderlib.utils.qthelpers import qapplication - app = qapplication() - test = EditorPluginExample() - test.resize(900, 700) - test.show() - import time - t0 = time.time() - test.load(__file__) - test.load("explorer.py") - test.load("dicteditor.py") - test.load("sourcecode/codeeditor.py") - test.load("../spyder.py") - print("Elapsed time: %.3f s" % (time.time()-t0)) - sys.exit(app.exec_()) - -if __name__ == "__main__": - test() +# -*- coding: utf-8 -*- +# +# Copyright © Spyder Project Contributors +# Licensed under the terms of the MIT License +# (see spyder/__init__.py for details) + +"""Editor Widget""" + +# pylint: disable=C0103 +# pylint: disable=R0903 +# pylint: disable=R0911 +# pylint: disable=R0201 + +# Local imports +from __future__ import print_function +import os +import os.path as osp +import sys + +# Third party imports +from qtpy import is_pyqt46 +from qtpy.compat import getsavefilename +from qtpy.QtCore import (QByteArray, QFileInfo, QObject, QPoint, QSize, Qt, + QThread, QTimer, Signal, Slot) +from qtpy.QtGui import QFont, QKeySequence +from qtpy.QtWidgets import (QAction, QApplication, QHBoxLayout, QMainWindow, + QMessageBox, QMenu, QSplitter, QVBoxLayout, + QWidget) + +# Local imports +from spyder.config.base import _, DEBUG, STDERR, STDOUT +from spyder.config.gui import (config_shortcut, fixed_shortcut, + RUN_CELL_SHORTCUT, + RUN_CELL_AND_ADVANCE_SHORTCUT) +from spyder.config.utils import (get_edit_filetypes, get_edit_filters, + get_filter) +from spyder.py3compat import qbytearray_to_str, to_text_string, u +from spyder.utils import icon_manager as ima +from spyder.utils import (codeanalysis, encoding, sourcecode, + syntaxhighlighters) +from spyder.utils.qthelpers import (add_actions, create_action, + create_toolbutton, mimedata2url) +from spyder.widgets.editortools import OutlineExplorerWidget +from spyder.widgets.fileswitcher import FileSwitcher +from spyder.widgets.findreplace import FindReplace +from spyder.widgets.sourcecode import codeeditor +from spyder.widgets.sourcecode.base import TextEditBaseWidget # analysis:ignore +from spyder.widgets.sourcecode.codeeditor import Printer # analysis:ignore +from spyder.widgets.sourcecode.codeeditor import get_file_language +from spyder.widgets.status import (CursorPositionStatus, EncodingStatus, + EOLStatus, ReadWriteStatus) +from spyder.widgets.tabs import BaseTabs + +DEBUG_EDITOR = DEBUG >= 3 + + +class AnalysisThread(QThread): + """Analysis thread""" + def __init__(self, parent, checker, source_code): + super(AnalysisThread, self).__init__(parent) + self.checker = checker + self.results = None + self.source_code = source_code + + def run(self): + """Run analysis""" + try: + self.results = self.checker(self.source_code) + except Exception: + if DEBUG_EDITOR: + import traceback + traceback.print_exc(file=STDERR) + + +class ThreadManager(QObject): + """Analysis thread manager""" + def __init__(self, parent, max_simultaneous_threads=2): + super(ThreadManager, self).__init__(parent) + self.max_simultaneous_threads = max_simultaneous_threads + self.started_threads = {} + self.pending_threads = [] + self.end_callbacks = {} + + def close_threads(self, parent): + """Close threads associated to parent_id""" + if DEBUG_EDITOR: + print("Call to 'close_threads'", file=STDOUT) + if parent is None: + # Closing all threads + self.pending_threads = [] + threadlist = [] + for threads in list(self.started_threads.values()): + threadlist += threads + else: + parent_id = id(parent) + self.pending_threads = [(_th, _id) for (_th, _id) + in self.pending_threads + if _id != parent_id] + threadlist = self.started_threads.get(parent_id, []) + for thread in threadlist: + if DEBUG_EDITOR: + print("Waiting for thread %r to finish" % thread, file=STDOUT) + while thread.isRunning(): + # We can't terminate thread safely, so we simply wait... + QApplication.processEvents() + + def close_all_threads(self): + """Close all threads""" + if DEBUG_EDITOR: + print("Call to 'close_all_threads'", file=STDOUT) + self.close_threads(None) + + def add_thread(self, checker, end_callback, source_code, parent): + """Add thread to queue""" + parent_id = id(parent) + thread = AnalysisThread(self, checker, source_code) + self.end_callbacks[id(thread)] = end_callback + self.pending_threads.append((thread, parent_id)) + if DEBUG_EDITOR: + print("Added thread %r to queue" % thread, file=STDOUT) + QTimer.singleShot(50, self.update_queue) + + def update_queue(self): + """Update queue""" + started = 0 + for parent_id, threadlist in list(self.started_threads.items()): + still_running = [] + for thread in threadlist: + if thread.isFinished(): + end_callback = self.end_callbacks.pop(id(thread)) + if thread.results is not None: + # The thread was executed successfully + end_callback(thread.results) + thread.setParent(None) + thread = None + else: + still_running.append(thread) + started += 1 + threadlist = None + if still_running: + self.started_threads[parent_id] = still_running + else: + self.started_threads.pop(parent_id) + if DEBUG_EDITOR: + print("Updating queue:", file=STDOUT) + print(" started:", started, file=STDOUT) + print(" pending:", len(self.pending_threads), file=STDOUT) + if self.pending_threads and started < self.max_simultaneous_threads: + thread, parent_id = self.pending_threads.pop(0) + thread.finished.connect(self.update_queue) + threadlist = self.started_threads.get(parent_id, []) + self.started_threads[parent_id] = threadlist+[thread] + if DEBUG_EDITOR: + print("===>starting:", thread, file=STDOUT) + thread.start() + + +class FileInfo(QObject): + """File properties""" + analysis_results_changed = Signal() + todo_results_changed = Signal() + save_breakpoints = Signal(str, str) + text_changed_at = Signal(str, int) + edit_goto = Signal(str, int, str) + send_to_help = Signal(str, str, str, str, bool) + + def __init__(self, filename, encoding, editor, new, threadmanager, + introspection_plugin): + QObject.__init__(self) + self.threadmanager = threadmanager + self.filename = filename + self.newly_created = new + self.default = False # Default untitled file + self.encoding = encoding + self.editor = editor + self.path = [] + + self.classes = (filename, None, None) + self.analysis_results = [] + self.todo_results = [] + self.lastmodified = QFileInfo(filename).lastModified() + + self.editor.textChanged.connect(self.text_changed) + self.editor.breakpoints_changed.connect(self.breakpoints_changed) + + self.pyflakes_results = None + self.pep8_results = None + + def text_changed(self): + """Editor's text has changed""" + self.default = False + self.text_changed_at.emit(self.filename, + self.editor.get_position('cursor')) + + def get_source_code(self): + """Return associated editor source code""" + return to_text_string(self.editor.toPlainText()) + + def run_code_analysis(self, run_pyflakes, run_pep8): + """Run code analysis""" + run_pyflakes = run_pyflakes and codeanalysis.is_pyflakes_installed() + run_pep8 = run_pep8 and\ + codeanalysis.get_checker_executable('pep8') is not None + self.pyflakes_results = [] + self.pep8_results = [] + if self.editor.is_python(): + enc = self.encoding.replace('-guessed', '').replace('-bom', '') + source_code = self.get_source_code().encode(enc) + if run_pyflakes: + self.pyflakes_results = None + if run_pep8: + self.pep8_results = None + if run_pyflakes: + self.threadmanager.add_thread(codeanalysis.check_with_pyflakes, + self.pyflakes_analysis_finished, + source_code, self) + if run_pep8: + self.threadmanager.add_thread(codeanalysis.check_with_pep8, + self.pep8_analysis_finished, + source_code, self) + + def pyflakes_analysis_finished(self, results): + """Pyflakes code analysis thread has finished""" + self.pyflakes_results = results + if self.pep8_results is not None: + self.code_analysis_finished() + + def pep8_analysis_finished(self, results): + """Pep8 code analysis thread has finished""" + self.pep8_results = results + if self.pyflakes_results is not None: + self.code_analysis_finished() + + def code_analysis_finished(self): + """Code analysis thread has finished""" + self.set_analysis_results(self.pyflakes_results+self.pep8_results) + self.analysis_results_changed.emit() + + def set_analysis_results(self, results): + """Set analysis results and update warning markers in editor""" + self.analysis_results = results + self.editor.process_code_analysis(results) + + def cleanup_analysis_results(self): + """Clean-up analysis results""" + self.analysis_results = [] + self.editor.cleanup_code_analysis() + + def run_todo_finder(self): + """Run TODO finder""" + if self.editor.is_python(): + self.threadmanager.add_thread(codeanalysis.find_tasks, + self.todo_finished, + self.get_source_code(), self) + + def todo_finished(self, results): + """Code analysis thread has finished""" + self.set_todo_results(results) + self.todo_results_changed.emit() + + def set_todo_results(self, results): + """Set TODO results and update markers in editor""" + self.todo_results = results + self.editor.process_todo(results) + + def cleanup_todo_results(self): + """Clean-up TODO finder results""" + self.todo_results = [] + + def breakpoints_changed(self): + """Breakpoint list has changed""" + breakpoints = self.editor.get_breakpoints() + if self.editor.breakpoints != breakpoints: + self.editor.breakpoints = breakpoints + self.save_breakpoints.emit(self.filename, repr(breakpoints)) + + +class EditorStack(QWidget): + reset_statusbar = Signal() + readonly_changed = Signal(bool) + encoding_changed = Signal(str) + sig_editor_cursor_position_changed = Signal(int, int) + refresh_eol_chars = Signal(str) + starting_long_process = Signal(str) + ending_long_process = Signal(str) + redirect_stdio = Signal(bool) + exec_in_extconsole = Signal(str, bool) + update_plugin_title = Signal() + editor_focus_changed = Signal() + zoom_in = Signal() + zoom_out = Signal() + zoom_reset = Signal() + sig_close_file = Signal(str, int) + file_saved = Signal(str, int, str) + file_renamed_in_data = Signal(str, int, str) + create_new_window = Signal() + opened_files_list_changed = Signal() + analysis_results_changed = Signal() + todo_results_changed = Signal() + update_code_analysis_actions = Signal() + refresh_file_dependent_actions = Signal() + refresh_save_all_action = Signal() + save_breakpoints = Signal(str, str) + text_changed_at = Signal(str, int) + current_file_changed = Signal(str ,int) + plugin_load = Signal((str,), ()) + edit_goto = Signal(str, int, str) + split_vertically = Signal() + split_horizontally = Signal() + sig_new_file = Signal((str,), ()) + sig_save_as = Signal() + sig_prev_edit_pos = Signal() + sig_prev_cursor = Signal() + sig_next_cursor = Signal() + + def __init__(self, parent, actions): + QWidget.__init__(self, parent) + + self.setAttribute(Qt.WA_DeleteOnClose) + + self.threadmanager = ThreadManager(self) + + self.newwindow_action = None + self.horsplit_action = None + self.versplit_action = None + self.close_action = None + self.__get_split_actions() + + layout = QVBoxLayout() + layout.setContentsMargins(0, 0, 0, 0) + self.setLayout(layout) + + self.menu = None + self.fileswitcher_dlg = None +# self.filelist_btn = None +# self.previous_btn = None +# self.next_btn = None + self.tabs = None + + self.stack_history = [] + + self.setup_editorstack(parent, layout) + + self.find_widget = None + + self.data = [] + fileswitcher_action = create_action(self, _("File switcher..."), + icon=ima.icon('filelist'), + triggered=self.open_fileswitcher_dlg) + copy_to_cb_action = create_action(self, _("Copy path to clipboard"), + icon=ima.icon('editcopy'), + triggered=lambda: + QApplication.clipboard().setText(self.get_current_filename())) + close_right = create_action(self, _("Close all to the right"), + triggered=self.close_all_right) + close_all_but_this = create_action(self, _("Close all but this"), + triggered=self.close_all_but_this) + + self.menu_actions = actions + [None, fileswitcher_action, + copy_to_cb_action, None, close_right, + close_all_but_this] + self.outlineexplorer = None + self.help = None + self.unregister_callback = None + self.is_closable = False + self.new_action = None + self.open_action = None + self.save_action = None + self.revert_action = None + self.tempfile_path = None + self.title = _("Editor") + self.pyflakes_enabled = True + self.pep8_enabled = False + self.todolist_enabled = True + self.realtime_analysis_enabled = False + self.is_analysis_done = False + self.linenumbers_enabled = True + self.blanks_enabled = False + self.edgeline_enabled = True + self.edgeline_column = 79 + self.codecompletion_auto_enabled = True + self.codecompletion_case_enabled = False + self.codecompletion_enter_enabled = False + self.calltips_enabled = True + self.go_to_definition_enabled = True + self.close_parentheses_enabled = True + self.close_quotes_enabled = True + self.add_colons_enabled = True + self.auto_unindent_enabled = True + self.indent_chars = " "*4 + self.tab_stop_width = 40 + self.help_enabled = False + self.default_font = None + self.wrap_enabled = False + self.tabmode_enabled = False + self.intelligent_backspace_enabled = True + self.highlight_current_line_enabled = False + self.highlight_current_cell_enabled = False + self.occurrence_highlighting_enabled = True + self.occurrence_highlighting_timeout=1500 + self.checkeolchars_enabled = True + self.always_remove_trailing_spaces = False + self.fullpath_sorting_enabled = None + self.focus_to_editor = True + self.set_fullpath_sorting_enabled(False) + self.create_new_file_if_empty = True + ccs = 'Spyder' + if ccs not in syntaxhighlighters.COLOR_SCHEME_NAMES: + ccs = syntaxhighlighters.COLOR_SCHEME_NAMES[0] + self.color_scheme = ccs + self.introspector = None + self.__file_status_flag = False + + # Real-time code analysis + self.analysis_timer = QTimer(self) + self.analysis_timer.setSingleShot(True) + self.analysis_timer.setInterval(2000) + self.analysis_timer.timeout.connect(self.analyze_script) + + # Accepting drops + self.setAcceptDrops(True) + + # Local shortcuts + self.shortcuts = self.create_shortcuts() + + def create_shortcuts(self): + """Create local shortcuts""" + # --- Configurable shortcuts + inspect = config_shortcut(self.inspect_current_object, context='Editor', + name='Inspect current object', parent=self) + set_breakpoint = config_shortcut(self.set_or_clear_breakpoint, + context='Editor', name='Breakpoint', + parent=self) + set_cond_breakpoint = config_shortcut( + self.set_or_edit_conditional_breakpoint, + context='Editor', + name='Conditional breakpoint', + parent=self) + gotoline = config_shortcut(self.go_to_line, context='Editor', + name='Go to line', parent=self) + tab = config_shortcut(self.go_to_previous_file, context='Editor', + name='Go to previous file', parent=self) + tabshift = config_shortcut(self.go_to_next_file, context='Editor', + name='Go to next file', parent=self) + run_selection = config_shortcut(self.run_selection, context='Editor', + name='Run selection', parent=self) + new_file = config_shortcut(lambda : self.sig_new_file[()].emit(), + context='Editor', name='New file', + parent=self) + open_file = config_shortcut(lambda : self.plugin_load[()].emit(), + context='Editor', name='Open file', + parent=self) + save_file = config_shortcut(self.save, context='Editor', + name='Save file', parent=self) + save_all = config_shortcut(self.save_all, context='Editor', + name='Save all', parent=self) + save_as = config_shortcut(lambda : self.sig_save_as.emit(), + context='Editor', name='Save As', + parent=self) + close_all = config_shortcut(self.close_all_files, context='Editor', + name='Close all', parent=self) + prev_edit_pos = config_shortcut(lambda : self.sig_prev_edit_pos.emit(), + context="Editor", + name="Last edit location", + parent=self) + prev_cursor = config_shortcut(lambda : self.sig_prev_cursor.emit(), + context="Editor", + name="Previous cursor position", + parent=self) + next_cursor = config_shortcut(lambda : self.sig_next_cursor.emit(), + context="Editor", + name="Next cursor position", + parent=self) + + # --- Fixed shortcuts + fixed_shortcut(QKeySequence.ZoomIn, self, lambda: self.zoom_in.emit()) + fixed_shortcut("Ctrl+=", self, lambda: self.zoom_in.emit()) + fixed_shortcut(QKeySequence.ZoomOut, self, lambda: self.zoom_out.emit()) + fixed_shortcut("Ctrl+0", self, lambda: self.zoom_reset.emit()) + fixed_shortcut("Ctrl+W", self, self.close_file) + fixed_shortcut("Ctrl+F4", self, self.close_file) + fixed_shortcut(QKeySequence(RUN_CELL_SHORTCUT), self, self.run_cell) + fixed_shortcut(QKeySequence(RUN_CELL_AND_ADVANCE_SHORTCUT), self, + self.run_cell_and_advance) + + # Return configurable ones + return [inspect, set_breakpoint, set_cond_breakpoint, gotoline, tab, + tabshift, run_selection, new_file, open_file, save_file, + save_all, save_as, close_all, prev_edit_pos, prev_cursor, + next_cursor] + + def get_shortcut_data(self): + """ + Returns shortcut data, a list of tuples (shortcut, text, default) + shortcut (QShortcut or QAction instance) + text (string): action/shortcut description + default (string): default key sequence + """ + return [sc.data for sc in self.shortcuts] + + def setup_editorstack(self, parent, layout): + """Setup editorstack's layout""" + menu_btn = create_toolbutton(self, icon=ima.icon('tooloptions'), + tip=_('Options')) + self.menu = QMenu(self) + menu_btn.setMenu(self.menu) + menu_btn.setPopupMode(menu_btn.InstantPopup) + self.menu.aboutToShow.connect(self.__setup_menu) + +# self.filelist_btn = create_toolbutton(self, +# icon=ima.icon('filelist'), +# tip=_("File list management"), +# triggered=self.open_fileswitcher_dlg) +# +# self.previous_btn = create_toolbutton(self, +# icon=ima.icon('previous'), +# tip=_("Previous file"), +# triggered=self.go_to_previous_file) +# +# self.next_btn = create_toolbutton(self, +# icon=ima.icon('next'), +# tip=_("Next file"), +# triggered=self.go_to_next_file) + + # Optional tabs +# corner_widgets = {Qt.TopRightCorner: [self.previous_btn, +# self.filelist_btn, self.next_btn, +# 5, menu_btn]} + corner_widgets = {Qt.TopRightCorner: [menu_btn]} + self.tabs = BaseTabs(self, menu=self.menu, menu_use_tooltips=True, + corner_widgets=corner_widgets) + self.tabs.tabBar().setObjectName('plugin-tab') + self.tabs.set_close_function(self.close_file) + + if hasattr(self.tabs, 'setDocumentMode') \ + and not sys.platform == 'darwin': + # Don't set document mode to true on OSX because it generates + # a crash when the editor is detached from the main window + # Fixes Issue 561 + self.tabs.setDocumentMode(True) + self.tabs.currentChanged.connect(self.current_changed) + + if sys.platform == 'darwin': + tab_container = QWidget() + tab_container.setObjectName('tab-container') + tab_layout = QHBoxLayout(tab_container) + tab_layout.setContentsMargins(0, 0, 0, 0) + tab_layout.addWidget(self.tabs) + layout.addWidget(tab_container) + else: + layout.addWidget(self.tabs) + + def add_corner_widgets_to_tabbar(self, widgets): + self.tabs.add_corner_widgets(widgets) + + def closeEvent(self, event): + self.threadmanager.close_all_threads() + self.analysis_timer.timeout.disconnect(self.analyze_script) + QWidget.closeEvent(self, event) + if is_pyqt46: + self.destroyed.emit() + + def clone_editor_from(self, other_finfo, set_current): + fname = other_finfo.filename + enc = other_finfo.encoding + new = other_finfo.newly_created + finfo = self.create_new_editor(fname, enc, "", + set_current=set_current, new=new, + cloned_from=other_finfo.editor) + finfo.set_analysis_results(other_finfo.analysis_results) + finfo.set_todo_results(other_finfo.todo_results) + return finfo.editor + + def clone_from(self, other): + """Clone EditorStack from other instance""" + for other_finfo in other.data: + self.clone_editor_from(other_finfo, set_current=True) + self.set_stack_index(other.get_stack_index()) + + @Slot() + def open_fileswitcher_dlg(self): + """Open file list management dialog box""" + if not self.tabs.count(): + return + if self.fileswitcher_dlg is not None and \ + self.fileswitcher_dlg.is_visible: + self.fileswitcher_dlg.hide() + self.fileswitcher_dlg.is_visible = False + return + self.fileswitcher_dlg = FileSwitcher(self, self.tabs, self.data) + self.fileswitcher_dlg.sig_goto_file.connect(self.set_stack_index) + self.fileswitcher_dlg.sig_close_file.connect(self.close_file) + self.fileswitcher_dlg.show() + self.fileswitcher_dlg.is_visible = True + + def update_fileswitcher_dlg(self): + """Synchronize file list dialog box with editor widget tabs""" + if self.fileswitcher_dlg: + self.fileswitcher_dlg.setup() + + def go_to_line(self): + """Go to line dialog""" + if self.data: + self.get_current_editor().exec_gotolinedialog() + + def set_or_clear_breakpoint(self): + """Set/clear breakpoint""" + if self.data: + editor = self.get_current_editor() + editor.add_remove_breakpoint() + + def set_or_edit_conditional_breakpoint(self): + """Set conditional breakpoint""" + if self.data: + editor = self.get_current_editor() + editor.add_remove_breakpoint(edit_condition=True) + + def inspect_current_object(self): + """Inspect current object in the Help plugin""" + if self.introspector: + editor = self.get_current_editor() + position = editor.get_position('cursor') + self.help.switch_to_editor_source() + self.introspector.show_object_info(position, auto=False) + else: + text = self.get_current_editor().get_current_object() + if text: + self.send_to_help(text, force=True) + + #------ Editor Widget Settings + def set_closable(self, state): + """Parent widget must handle the closable state""" + self.is_closable = state + + def set_io_actions(self, new_action, open_action, + save_action, revert_action): + self.new_action = new_action + self.open_action = open_action + self.save_action = save_action + self.revert_action = revert_action + + def set_find_widget(self, find_widget): + self.find_widget = find_widget + + def set_outlineexplorer(self, outlineexplorer): + self.outlineexplorer = outlineexplorer + self.outlineexplorer.outlineexplorer_is_visible.connect( + self._refresh_outlineexplorer) + + def initialize_outlineexplorer(self): + """This method is called separately from 'set_oulineexplorer' to avoid + doing unnecessary updates when there are multiple editor windows""" + for index in range(self.get_stack_count()): + if index != self.get_stack_index(): + self._refresh_outlineexplorer(index=index) + + def add_outlineexplorer_button(self, editor_plugin): + oe_btn = create_toolbutton(editor_plugin) + oe_btn.setDefaultAction(self.outlineexplorer.visibility_action) + self.add_corner_widgets_to_tabbar([5, oe_btn]) + + def set_help(self, help_plugin): + self.help = help_plugin + + def set_tempfile_path(self, path): + self.tempfile_path = path + + def set_title(self, text): + self.title = text + + def __update_editor_margins(self, editor): + editor.setup_margins(linenumbers=self.linenumbers_enabled, + markers=self.has_markers()) + + def __codeanalysis_settings_changed(self, current_finfo): + if self.data: + run_pyflakes, run_pep8 = self.pyflakes_enabled, self.pep8_enabled + for finfo in self.data: + self.__update_editor_margins(finfo.editor) + finfo.cleanup_analysis_results() + if (run_pyflakes or run_pep8) and current_finfo is not None: + if current_finfo is not finfo: + finfo.run_code_analysis(run_pyflakes, run_pep8) + + def set_pyflakes_enabled(self, state, current_finfo=None): + # CONF.get(self.CONF_SECTION, 'code_analysis/pyflakes') + self.pyflakes_enabled = state + self.__codeanalysis_settings_changed(current_finfo) + + def set_pep8_enabled(self, state, current_finfo=None): + # CONF.get(self.CONF_SECTION, 'code_analysis/pep8') + self.pep8_enabled = state + self.__codeanalysis_settings_changed(current_finfo) + + def has_markers(self): + """Return True if this editorstack has a marker margin for TODOs or + code analysis""" + return self.todolist_enabled or self.pyflakes_enabled\ + or self.pep8_enabled + + def set_todolist_enabled(self, state, current_finfo=None): + # CONF.get(self.CONF_SECTION, 'todo_list') + self.todolist_enabled = state + if self.data: + for finfo in self.data: + self.__update_editor_margins(finfo.editor) + finfo.cleanup_todo_results() + if state and current_finfo is not None: + if current_finfo is not finfo: + finfo.run_todo_finder() + + def set_realtime_analysis_enabled(self, state): + self.realtime_analysis_enabled = state + + def set_realtime_analysis_timeout(self, timeout): + self.analysis_timer.setInterval(timeout) + + def set_linenumbers_enabled(self, state, current_finfo=None): + # CONF.get(self.CONF_SECTION, 'line_numbers') + self.linenumbers_enabled = state + if self.data: + for finfo in self.data: + self.__update_editor_margins(finfo.editor) + + def set_blanks_enabled(self, state): + self.blanks_enabled = state + if self.data: + for finfo in self.data: + finfo.editor.set_blanks_enabled(state) + + def set_edgeline_enabled(self, state): + # CONF.get(self.CONF_SECTION, 'edge_line') + self.edgeline_enabled = state + if self.data: + for finfo in self.data: + finfo.editor.set_edge_line_enabled(state) + + def set_edgeline_column(self, column): + # CONF.get(self.CONF_SECTION, 'edge_line_column') + self.edgeline_column = column + if self.data: + for finfo in self.data: + finfo.editor.set_edge_line_column(column) + + def set_codecompletion_auto_enabled(self, state): + # CONF.get(self.CONF_SECTION, 'codecompletion_auto') + self.codecompletion_auto_enabled = state + if self.data: + for finfo in self.data: + finfo.editor.set_codecompletion_auto(state) + + def set_codecompletion_case_enabled(self, state): + self.codecompletion_case_enabled = state + if self.data: + for finfo in self.data: + finfo.editor.set_codecompletion_case(state) + + def set_codecompletion_enter_enabled(self, state): + self.codecompletion_enter_enabled = state + if self.data: + for finfo in self.data: + finfo.editor.set_codecompletion_enter(state) + + def set_calltips_enabled(self, state): + # CONF.get(self.CONF_SECTION, 'calltips') + self.calltips_enabled = state + if self.data: + for finfo in self.data: + finfo.editor.set_calltips(state) + + def set_go_to_definition_enabled(self, state): + # CONF.get(self.CONF_SECTION, 'go_to_definition') + self.go_to_definition_enabled = state + if self.data: + for finfo in self.data: + finfo.editor.set_go_to_definition_enabled(state) + + def set_close_parentheses_enabled(self, state): + # CONF.get(self.CONF_SECTION, 'close_parentheses') + self.close_parentheses_enabled = state + if self.data: + for finfo in self.data: + finfo.editor.set_close_parentheses_enabled(state) + + def set_close_quotes_enabled(self, state): + # CONF.get(self.CONF_SECTION, 'close_quotes') + self.close_quotes_enabled = state + if self.data: + for finfo in self.data: + finfo.editor.set_close_quotes_enabled(state) + + def set_add_colons_enabled(self, state): + # CONF.get(self.CONF_SECTION, 'add_colons') + self.add_colons_enabled = state + if self.data: + for finfo in self.data: + finfo.editor.set_add_colons_enabled(state) + + def set_auto_unindent_enabled(self, state): + # CONF.get(self.CONF_SECTION, 'auto_unindent') + self.auto_unindent_enabled = state + if self.data: + for finfo in self.data: + finfo.editor.set_auto_unindent_enabled(state) + + def set_indent_chars(self, indent_chars): + # CONF.get(self.CONF_SECTION, 'indent_chars') + indent_chars = indent_chars[1:-1] # removing the leading/ending '*' + self.indent_chars = indent_chars + if self.data: + for finfo in self.data: + finfo.editor.set_indent_chars(indent_chars) + + def set_tab_stop_width(self, tab_stop_width): + # CONF.get(self.CONF_SECTION, 'tab_stop_width') + self.tab_stop_width = tab_stop_width + if self.data: + for finfo in self.data: + finfo.editor.setTabStopWidth(tab_stop_width) + + def set_help_enabled(self, state): + self.help_enabled = state + + def set_default_font(self, font, color_scheme=None): + self.default_font = font + if color_scheme is not None: + self.color_scheme = color_scheme + if self.data: + for finfo in self.data: + finfo.editor.set_font(font, color_scheme) + + def set_color_scheme(self, color_scheme): + self.color_scheme = color_scheme + if self.data: + for finfo in self.data: + finfo.editor.set_color_scheme(color_scheme) + + def set_wrap_enabled(self, state): + # CONF.get(self.CONF_SECTION, 'wrap') + self.wrap_enabled = state + if self.data: + for finfo in self.data: + finfo.editor.toggle_wrap_mode(state) + + def set_tabmode_enabled(self, state): + # CONF.get(self.CONF_SECTION, 'tab_always_indent') + self.tabmode_enabled = state + if self.data: + for finfo in self.data: + finfo.editor.set_tab_mode(state) + + def set_intelligent_backspace_enabled(self, state): + # CONF.get(self.CONF_SECTION, 'intelligent_backspace') + self.intelligent_backspace_enabled = state + if self.data: + for finfo in self.data: + finfo.editor.toggle_intelligent_backspace(state) + + def set_occurrence_highlighting_enabled(self, state): + # CONF.get(self.CONF_SECTION, 'occurrence_highlighting') + self.occurrence_highlighting_enabled = state + if self.data: + for finfo in self.data: + finfo.editor.set_occurrence_highlighting(state) + + def set_occurrence_highlighting_timeout(self, timeout): + # CONF.get(self.CONF_SECTION, 'occurrence_highlighting/timeout') + self.occurrence_highlighting_timeout = timeout + if self.data: + for finfo in self.data: + finfo.editor.set_occurrence_timeout(timeout) + + def set_highlight_current_line_enabled(self, state): + self.highlight_current_line_enabled = state + if self.data: + for finfo in self.data: + finfo.editor.set_highlight_current_line(state) + + def set_highlight_current_cell_enabled(self, state): + self.highlight_current_cell_enabled = state + if self.data: + for finfo in self.data: + finfo.editor.set_highlight_current_cell(state) + + def set_checkeolchars_enabled(self, state): + # CONF.get(self.CONF_SECTION, 'check_eol_chars') + self.checkeolchars_enabled = state + + def set_fullpath_sorting_enabled(self, state): + # CONF.get(self.CONF_SECTION, 'fullpath_sorting') + self.fullpath_sorting_enabled = state + if self.data: + finfo = self.data[self.get_stack_index()] + self.data.sort(key=self.__get_sorting_func()) + new_index = self.data.index(finfo) + self.__repopulate_stack() + self.set_stack_index(new_index) + + def set_always_remove_trailing_spaces(self, state): + # CONF.get(self.CONF_SECTION, 'always_remove_trailing_spaces') + self.always_remove_trailing_spaces = state + + def set_focus_to_editor(self, state): + self.focus_to_editor = state + + def set_introspector(self, introspector): + self.introspector = introspector + + #------ Stacked widget management + def get_stack_index(self): + return self.tabs.currentIndex() + + def get_current_finfo(self): + if self.data: + return self.data[self.get_stack_index()] + + def get_current_editor(self): + return self.tabs.currentWidget() + + def get_stack_count(self): + return self.tabs.count() + + def set_stack_index(self, index): + self.tabs.setCurrentIndex(index) + + def set_tabbar_visible(self, state): + self.tabs.tabBar().setVisible(state) + + def remove_from_data(self, index): + self.tabs.blockSignals(True) + self.tabs.removeTab(index) + self.data.pop(index) + self.tabs.blockSignals(False) + self.update_actions() + self.update_fileswitcher_dlg() + + def __modified_readonly_title(self, title, is_modified, is_readonly): + if is_modified is not None and is_modified: + title += "*" + if is_readonly is not None and is_readonly: + title = "(%s)" % title + return title + + def get_tab_text(self, filename, is_modified=None, is_readonly=None): + """Return tab title""" + return self.__modified_readonly_title(osp.basename(filename), + is_modified, is_readonly) + + def get_tab_tip(self, filename, is_modified=None, is_readonly=None): + """Return tab menu title""" + if self.fullpath_sorting_enabled: + text = filename + else: + text = u("%s — %s") + text = self.__modified_readonly_title(text, + is_modified, is_readonly) + if self.tempfile_path is not None\ + and filename == encoding.to_unicode_from_fs(self.tempfile_path): + temp_file_str = to_text_string(_("Temporary file")) + if self.fullpath_sorting_enabled: + return "%s (%s)" % (text, temp_file_str) + else: + return text % (temp_file_str, self.tempfile_path) + else: + if self.fullpath_sorting_enabled: + return text + else: + return text % (osp.basename(filename), osp.dirname(filename)) + + def __get_sorting_func(self): + if self.fullpath_sorting_enabled: + return lambda item: osp.join(osp.dirname(item.filename), + '_'+osp.basename(item.filename)) + else: + return lambda item: osp.basename(item.filename) + + def add_to_data(self, finfo, set_current): + self.data.append(finfo) + self.data.sort(key=self.__get_sorting_func()) + index = self.data.index(finfo) + fname, editor = finfo.filename, finfo.editor + self.tabs.insertTab(index, editor, self.get_tab_text(fname)) + self.set_stack_title(index, False) + if set_current: + self.set_stack_index(index) + self.current_changed(index) + self.update_actions() + self.update_fileswitcher_dlg() + + def __repopulate_stack(self): + self.tabs.blockSignals(True) + self.tabs.clear() + for finfo in self.data: + if finfo.newly_created: + is_modified = True + else: + is_modified = None + tab_text = self.get_tab_text(finfo.filename, is_modified) + tab_tip = self.get_tab_tip(finfo.filename) + index = self.tabs.addTab(finfo.editor, tab_text) + self.tabs.setTabToolTip(index, tab_tip) + self.tabs.blockSignals(False) + self.update_fileswitcher_dlg() + + def rename_in_data(self, index, new_filename): + finfo = self.data[index] + if osp.splitext(finfo.filename)[1] != osp.splitext(new_filename)[1]: + # File type has changed! + txt = to_text_string(finfo.editor.get_text_with_eol()) + language = get_file_language(new_filename, txt) + finfo.editor.set_language(language) + set_new_index = index == self.get_stack_index() + current_fname = self.get_current_filename() + finfo.filename = new_filename + self.data.sort(key=self.__get_sorting_func()) + new_index = self.data.index(finfo) + self.__repopulate_stack() + if set_new_index: + self.set_stack_index(new_index) + else: + # Fixes Issue 1287 + self.set_current_filename(current_fname) + if self.outlineexplorer is not None: + self.outlineexplorer.file_renamed(finfo.editor, finfo.filename) + return new_index + + def set_stack_title(self, index, is_modified): + finfo = self.data[index] + fname = finfo.filename + is_modified = (is_modified or finfo.newly_created) and not finfo.default + is_readonly = finfo.editor.isReadOnly() + tab_text = self.get_tab_text(fname, is_modified, is_readonly) + tab_tip = self.get_tab_tip(fname, is_modified, is_readonly) + self.tabs.setTabText(index, tab_text) + self.tabs.setTabToolTip(index, tab_tip) + + + #------ Context menu + def __setup_menu(self): + """Setup tab context menu before showing it""" + self.menu.clear() + if self.data: + actions = self.menu_actions + else: + actions = (self.new_action, self.open_action) + self.setFocus() # --> Editor.__get_focus_editortabwidget + add_actions(self.menu, list(actions)+self.__get_split_actions()) + self.close_action.setEnabled(self.is_closable) + + + #------ Hor/Ver splitting + def __get_split_actions(self): + # New window + self.newwindow_action = create_action(self, _("New window"), + icon=ima.icon('newwindow'), tip=_("Create a new editor window"), + triggered=lambda: self.create_new_window.emit()) + # Splitting + self.versplit_action = create_action(self, _("Split vertically"), + icon=ima.icon('versplit'), + tip=_("Split vertically this editor window"), + triggered=lambda: self.split_vertically.emit()) + self.horsplit_action = create_action(self, _("Split horizontally"), + icon=ima.icon('horsplit'), + tip=_("Split horizontally this editor window"), + triggered=lambda: self.split_horizontally.emit()) + self.close_action = create_action(self, _("Close this panel"), + icon=ima.icon('close_panel'), triggered=self.close) + return [None, self.newwindow_action, None, + self.versplit_action, self.horsplit_action, self.close_action] + + def reset_orientation(self): + self.horsplit_action.setEnabled(True) + self.versplit_action.setEnabled(True) + + def set_orientation(self, orientation): + self.horsplit_action.setEnabled(orientation == Qt.Horizontal) + self.versplit_action.setEnabled(orientation == Qt.Vertical) + + def update_actions(self): + state = self.get_stack_count() > 0 + self.horsplit_action.setEnabled(state) + self.versplit_action.setEnabled(state) + + + #------ Accessors + def get_current_filename(self): + if self.data: + return self.data[self.get_stack_index()].filename + + def has_filename(self, filename): + fixpath = lambda path: osp.normcase(osp.realpath(path)) + for index, finfo in enumerate(self.data): + if fixpath(filename) == fixpath(finfo.filename): + return index + + def set_current_filename(self, filename): + """Set current filename and return the associated editor instance""" + index = self.has_filename(filename) + if index is not None: + self.set_stack_index(index) + editor = self.data[index].editor + editor.setFocus() + return editor + + def is_file_opened(self, filename=None): + if filename is None: + # Is there any file opened? + return len(self.data) > 0 + else: + return self.has_filename(filename) + + + #------ Close file, tabwidget... + def close_file(self, index=None, force=False): + """Close file (index=None -> close current file) + Keep current file index unchanged (if current file + that is being closed)""" + current_index = self.get_stack_index() + count = self.get_stack_count() + + if index is None: + if count > 0: + index = current_index + else: + self.find_widget.set_editor(None) + return + + new_index = None + if count > 1: + if current_index == index: + new_index = self._get_previous_file_index() + else: + new_index = current_index + + is_ok = force or self.save_if_changed(cancelable=True, index=index) + if is_ok: + finfo = self.data[index] + self.threadmanager.close_threads(finfo) + # Removing editor reference from outline explorer settings: + if self.outlineexplorer is not None: + self.outlineexplorer.remove_editor(finfo.editor) + + self.remove_from_data(index) + + # We pass self object ID as a QString, because otherwise it would + # depend on the platform: long for 64bit, int for 32bit. Replacing + # by long all the time is not working on some 32bit platforms + # (see Issue 1094, Issue 1098) + self.sig_close_file.emit(str(id(self)), index) + + if not self.data and self.is_closable: + # editortabwidget is empty: removing it + # (if it's not the first editortabwidget) + self.close() + + self.opened_files_list_changed.emit() + self.update_code_analysis_actions.emit() + self._refresh_outlineexplorer() + self.refresh_file_dependent_actions.emit() + self.update_plugin_title.emit() + + editor = self.get_current_editor() + if editor: + editor.setFocus() + + if new_index is not None: + if index < new_index: + new_index -= 1 + self.set_stack_index(new_index) + + if self.get_stack_count() == 0 and self.create_new_file_if_empty: + self.sig_new_file[()].emit() + return False + + return is_ok + + def close_all_files(self): + """Close all opened scripts""" + while self.close_file(): + pass + + def close_all_right(self): + """ Close all files opened to the right """ + num = self.get_stack_index() + n = self.get_stack_count() + for i in range(num, n-1): + self.close_file(num+1) + + def close_all_but_this(self): + """Close all files but the current one""" + self.close_all_right() + for i in range(0, self.get_stack_count()-1 ): + self.close_file(0) + + #------ Save + def save_if_changed(self, cancelable=False, index=None): + """Ask user to save file if modified""" + if index is None: + indexes = list(range(self.get_stack_count())) + else: + indexes = [index] + buttons = QMessageBox.Yes | QMessageBox.No + if cancelable: + buttons |= QMessageBox.Cancel + unsaved_nb = 0 + for index in indexes: + if self.data[index].editor.document().isModified(): + unsaved_nb += 1 + if not unsaved_nb: + # No file to save + return True + if unsaved_nb > 1: + buttons |= QMessageBox.YesAll | QMessageBox.NoAll + yes_all = False + for index in indexes: + self.set_stack_index(index) + finfo = self.data[index] + if finfo.filename == self.tempfile_path or yes_all: + if not self.save(): + return False + elif finfo.editor.document().isModified(): + answer = QMessageBox.question(self, self.title, + _("%s has been modified." + "
    Do you want to save changes?" + ) % osp.basename(finfo.filename), + buttons) + if answer == QMessageBox.Yes: + if not self.save(): + return False + elif answer == QMessageBox.YesAll: + if not self.save(): + return False + yes_all = True + elif answer == QMessageBox.NoAll: + return True + elif answer == QMessageBox.Cancel: + return False + return True + + def save(self, index=None, force=False): + """Save file""" + if index is None: + # Save the currently edited file + if not self.get_stack_count(): + return + index = self.get_stack_index() + + finfo = self.data[index] + if not finfo.editor.document().isModified() and not force: + return True + if not osp.isfile(finfo.filename) and not force: + # File has not been saved yet + return self.save_as(index=index) + if self.always_remove_trailing_spaces: + self.remove_trailing_spaces(index) + txt = to_text_string(finfo.editor.get_text_with_eol()) + try: + finfo.encoding = encoding.write(txt, finfo.filename, + finfo.encoding) + finfo.newly_created = False + self.encoding_changed.emit(finfo.encoding) + finfo.lastmodified = QFileInfo(finfo.filename).lastModified() + + # We pass self object ID as a QString, because otherwise it would + # depend on the platform: long for 64bit, int for 32bit. Replacing + # by long all the time is not working on some 32bit platforms + # (see Issue 1094, Issue 1098) + self.file_saved.emit(str(id(self)), index, finfo.filename) + + finfo.editor.document().setModified(False) + self.modification_changed(index=index) + self.analyze_script(index) + self.introspector.validate() + + #XXX CodeEditor-only: re-scan the whole text to rebuild outline + # explorer data from scratch (could be optimized because + # rehighlighting text means searching for all syntax coloring + # patterns instead of only searching for class/def patterns which + # would be sufficient for outline explorer data. + finfo.editor.rehighlight() + + self._refresh_outlineexplorer(index) + return True + except EnvironmentError as error: + QMessageBox.critical(self, _("Save"), + _("Unable to save script '%s'" + "

    Error message:
    %s" + ) % (osp.basename(finfo.filename), + str(error))) + return False + + def file_saved_in_other_editorstack(self, index, filename): + """ + File was just saved in another editorstack, let's synchronize! + This avoid file to be automatically reloaded + + Filename is passed in case file was just saved as another name + """ + finfo = self.data[index] + finfo.newly_created = False + finfo.filename = to_text_string(filename) + finfo.lastmodified = QFileInfo(finfo.filename).lastModified() + + def select_savename(self, original_filename): + self.redirect_stdio.emit(False) + filename, _selfilter = getsavefilename(self, _("Save file"), + original_filename, + get_edit_filters(), + get_filter(get_edit_filetypes(), + osp.splitext(original_filename)[1])) + self.redirect_stdio.emit(True) + if filename: + return osp.normpath(filename) + + def save_as(self, index=None): + """Save file as...""" + if index is None: + # Save the currently edited file + index = self.get_stack_index() + finfo = self.data[index] + filename = self.select_savename(finfo.filename) + if filename: + ao_index = self.has_filename(filename) + # Note: ao_index == index --> saving an untitled file + if ao_index and ao_index != index: + if not self.close_file(ao_index): + return + if ao_index < index: + index -= 1 + + new_index = self.rename_in_data(index, new_filename=filename) + + # We pass self object ID as a QString, because otherwise it would + # depend on the platform: long for 64bit, int for 32bit. Replacing + # by long all the time is not working on some 32bit platforms + # (see Issue 1094, Issue 1098) + self.file_renamed_in_data.emit(str(id(self)), index, filename) + + ok = self.save(index=new_index, force=True) + self.refresh(new_index) + self.set_stack_index(new_index) + return ok + else: + return False + + def save_all(self): + """Save all opened files""" + folders = set() + for index in range(self.get_stack_count()): + if self.data[index].editor.document().isModified(): + folders.add(osp.dirname(self.data[index].filename)) + self.save(index) + + #------ Update UI + def start_stop_analysis_timer(self): + self.is_analysis_done = False + if self.realtime_analysis_enabled: + self.analysis_timer.stop() + self.analysis_timer.start() + + def analyze_script(self, index=None): + """Analyze current script with pyflakes + find todos""" + if self.is_analysis_done: + return + if index is None: + index = self.get_stack_index() + if self.data: + finfo = self.data[index] + run_pyflakes, run_pep8 = self.pyflakes_enabled, self.pep8_enabled + if run_pyflakes or run_pep8: + finfo.run_code_analysis(run_pyflakes, run_pep8) + if self.todolist_enabled: + finfo.run_todo_finder() + self.is_analysis_done = True + + def set_analysis_results(self, index, analysis_results): + """Synchronize analysis results between editorstacks""" + self.data[index].set_analysis_results(analysis_results) + + def get_analysis_results(self): + if self.data: + return self.data[self.get_stack_index()].analysis_results + + def set_todo_results(self, index, todo_results): + """Synchronize todo results between editorstacks""" + self.data[index].set_todo_results(todo_results) + + def get_todo_results(self): + if self.data: + return self.data[self.get_stack_index()].todo_results + + def current_changed(self, index): + """Stack index has changed""" +# count = self.get_stack_count() +# for btn in (self.filelist_btn, self.previous_btn, self.next_btn): +# btn.setEnabled(count > 1) + + editor = self.get_current_editor() + if index != -1: + editor.setFocus() + if DEBUG_EDITOR: + print("setfocusto:", editor, file=STDOUT) + else: + self.reset_statusbar.emit() + self.opened_files_list_changed.emit() + + # Index history management + id_list = [id(self.tabs.widget(_i)) + for _i in range(self.tabs.count())] + for _id in self.stack_history[:]: + if _id not in id_list: + self.stack_history.pop(self.stack_history.index(_id)) + current_id = id(self.tabs.widget(index)) + while current_id in self.stack_history: + self.stack_history.pop(self.stack_history.index(current_id)) + self.stack_history.append(current_id) + if DEBUG_EDITOR: + print("current_changed:", index, self.data[index].editor, end=' ', file=STDOUT) + print(self.data[index].editor.get_document_id(), file=STDOUT) + + self.update_plugin_title.emit() + if editor is not None: + self.current_file_changed.emit(self.data[index].filename, + editor.get_position('cursor')) + + def _get_previous_file_index(self): + if len(self.stack_history) > 1: + last = len(self.stack_history)-1 + w_id = self.stack_history.pop(last) + self.stack_history.insert(0, w_id) + last_id = self.stack_history[last] + for _i in range(self.tabs.count()): + if id(self.tabs.widget(_i)) == last_id: + return _i + + def go_to_previous_file(self): + """Ctrl+Tab""" + prev_index = self._get_previous_file_index() + if prev_index is not None: + self.set_stack_index(prev_index) + elif len(self.stack_history) == 0 and self.get_stack_count(): + self.stack_history = [id(self.tabs.currentWidget())] + + def go_to_next_file(self): + """Ctrl+Shift+Tab""" + if len(self.stack_history) > 1: + last = len(self.stack_history)-1 + w_id = self.stack_history.pop(0) + self.stack_history.append(w_id) + last_id = self.stack_history[last] + for _i in range(self.tabs.count()): + if id(self.tabs.widget(_i)) == last_id: + self.set_stack_index(_i) + break + elif len(self.stack_history) == 0 and self.get_stack_count(): + self.stack_history = [id(self.tabs.currentWidget())] + + def focus_changed(self): + """Editor focus has changed""" + fwidget = QApplication.focusWidget() + for finfo in self.data: + if fwidget is finfo.editor: + self.refresh() + self.editor_focus_changed.emit() + + def _refresh_outlineexplorer(self, index=None, update=True, clear=False): + """Refresh outline explorer panel""" + oe = self.outlineexplorer + if oe is None: + return + if index is None: + index = self.get_stack_index() + enable = False + if self.data: + finfo = self.data[index] + if finfo.editor.is_python(): + enable = True + oe.setEnabled(True) + oe.set_current_editor(finfo.editor, finfo.filename, + update=update, clear=clear) + if not enable: + oe.setEnabled(False) + + def __refresh_statusbar(self, index): + """Refreshing statusbar widgets""" + finfo = self.data[index] + self.encoding_changed.emit(finfo.encoding) + # Refresh cursor position status: + line, index = finfo.editor.get_cursor_line_column() + self.sig_editor_cursor_position_changed.emit(line, index) + + def __refresh_readonly(self, index): + finfo = self.data[index] + read_only = not QFileInfo(finfo.filename).isWritable() + if not osp.isfile(finfo.filename): + # This is an 'untitledX.py' file (newly created) + read_only = False + finfo.editor.setReadOnly(read_only) + self.readonly_changed.emit(read_only) + + def __check_file_status(self, index): + """Check if file has been changed in any way outside Spyder: + 1. removed, moved or renamed outside Spyder + 2. modified outside Spyder""" + if self.__file_status_flag: + # Avoid infinite loop: when the QMessageBox.question pops, it + # gets focus and then give it back to the CodeEditor instance, + # triggering a refresh cycle which calls this method + return + self.__file_status_flag = True + + finfo = self.data[index] + name = osp.basename(finfo.filename) + + if finfo.newly_created: + # File was just created (not yet saved): do nothing + # (do not return because of the clean-up at the end of the method) + pass + + elif not osp.isfile(finfo.filename): + # File doesn't exist (removed, moved or offline): + answer = QMessageBox.warning(self, self.title, + _("%s is unavailable " + "(this file may have been removed, moved " + "or renamed outside Spyder)." + "
    Do you want to close it?") % name, + QMessageBox.Yes | QMessageBox.No) + if answer == QMessageBox.Yes: + self.close_file(index) + else: + finfo.newly_created = True + finfo.editor.document().setModified(True) + self.modification_changed(index=index) + + else: + # Else, testing if it has been modified elsewhere: + lastm = QFileInfo(finfo.filename).lastModified() + if to_text_string(lastm.toString()) \ + != to_text_string(finfo.lastmodified.toString()): + if finfo.editor.document().isModified(): + answer = QMessageBox.question(self, + self.title, + _("%s has been modified outside Spyder." + "
    Do you want to reload it and lose all " + "your changes?") % name, + QMessageBox.Yes | QMessageBox.No) + if answer == QMessageBox.Yes: + self.reload(index) + else: + finfo.lastmodified = lastm + else: + self.reload(index) + + # Finally, resetting temporary flag: + self.__file_status_flag = False + + def refresh(self, index=None): + """Refresh tabwidget""" + if index is None: + index = self.get_stack_index() + # Set current editor + if self.get_stack_count(): + index = self.get_stack_index() + finfo = self.data[index] + editor = finfo.editor + editor.setFocus() + self._refresh_outlineexplorer(index, update=False) + self.update_code_analysis_actions.emit() + self.__refresh_statusbar(index) + self.__refresh_readonly(index) + self.__check_file_status(index) + self.update_plugin_title.emit() + else: + editor = None + # Update the modification-state-dependent parameters + self.modification_changed() + # Update FindReplace binding + self.find_widget.set_editor(editor, refresh=False) + + def modification_changed(self, state=None, index=None, editor_id=None): + """ + Current editor's modification state has changed + --> change tab title depending on new modification state + --> enable/disable save/save all actions + """ + if editor_id is not None: + for index, _finfo in enumerate(self.data): + if id(_finfo.editor) == editor_id: + break + # This must be done before refreshing save/save all actions: + # (otherwise Save/Save all actions will always be enabled) + self.opened_files_list_changed.emit() + # -- + if index is None: + index = self.get_stack_index() + if index == -1: + return + finfo = self.data[index] + if state is None: + state = finfo.editor.document().isModified() + self.set_stack_title(index, state) + # Toggle save/save all actions state + self.save_action.setEnabled(state) + self.refresh_save_all_action.emit() + # Refreshing eol mode + eol_chars = finfo.editor.get_line_separator() + os_name = sourcecode.get_os_name_from_eol_chars(eol_chars) + self.refresh_eol_chars.emit(os_name) + + + #------ Load, reload + def reload(self, index): + """Reload file from disk""" + finfo = self.data[index] + txt, finfo.encoding = encoding.read(finfo.filename) + finfo.lastmodified = QFileInfo(finfo.filename).lastModified() + position = finfo.editor.get_position('cursor') + finfo.editor.set_text(txt) + finfo.editor.document().setModified(False) + finfo.editor.set_cursor_position(position) + self.introspector.validate() + + #XXX CodeEditor-only: re-scan the whole text to rebuild outline + # explorer data from scratch (could be optimized because + # rehighlighting text means searching for all syntax coloring + # patterns instead of only searching for class/def patterns which + # would be sufficient for outline explorer data. + finfo.editor.rehighlight() + + self._refresh_outlineexplorer(index) + + def revert(self): + """Revert file from disk""" + index = self.get_stack_index() + finfo = self.data[index] + filename = finfo.filename + if finfo.editor.document().isModified(): + answer = QMessageBox.warning(self, self.title, + _("All changes to %s will be lost." + "
    Do you want to revert file from disk?" + ) % osp.basename(filename), + QMessageBox.Yes|QMessageBox.No) + if answer != QMessageBox.Yes: + return + self.reload(index) + + def create_new_editor(self, fname, enc, txt, set_current, new=False, + cloned_from=None): + """ + Create a new editor instance + Returns finfo object (instead of editor as in previous releases) + """ + editor = codeeditor.CodeEditor(self) + introspector = self.introspector + editor.get_completions.connect(introspector.get_completions) + editor.sig_show_object_info.connect(introspector.show_object_info) + editor.go_to_definition.connect(introspector.go_to_definition) + + finfo = FileInfo(fname, enc, editor, new, self.threadmanager, + self.introspector) + + self.add_to_data(finfo, set_current) + finfo.send_to_help.connect(self.send_to_help) + finfo.analysis_results_changed.connect( + lambda: self.analysis_results_changed.emit()) + finfo.todo_results_changed.connect( + lambda: self.todo_results_changed.emit()) + finfo.edit_goto.connect(lambda fname, lineno, name: + self.edit_goto.emit(fname, lineno, name)) + finfo.save_breakpoints.connect(lambda s1, s2: + self.save_breakpoints.emit(s1, s2)) + editor.run_selection.connect(self.run_selection) + editor.run_cell.connect(self.run_cell) + editor.run_cell_and_advance.connect(self.run_cell_and_advance) + editor.sig_new_file.connect(self.sig_new_file.emit) + language = get_file_language(fname, txt) + editor.setup_editor( + linenumbers=self.linenumbers_enabled, + show_blanks=self.blanks_enabled, + edge_line=self.edgeline_enabled, + edge_line_column=self.edgeline_column, language=language, + markers=self.has_markers(), font=self.default_font, + color_scheme=self.color_scheme, + wrap=self.wrap_enabled, tab_mode=self.tabmode_enabled, + intelligent_backspace=self.intelligent_backspace_enabled, + highlight_current_line=self.highlight_current_line_enabled, + highlight_current_cell=self.highlight_current_cell_enabled, + occurrence_highlighting=self.occurrence_highlighting_enabled, + occurrence_timeout=self.occurrence_highlighting_timeout, + codecompletion_auto=self.codecompletion_auto_enabled, + codecompletion_case=self.codecompletion_case_enabled, + codecompletion_enter=self.codecompletion_enter_enabled, + calltips=self.calltips_enabled, + go_to_definition=self.go_to_definition_enabled, + close_parentheses=self.close_parentheses_enabled, + close_quotes=self.close_quotes_enabled, + add_colons=self.add_colons_enabled, + auto_unindent=self.auto_unindent_enabled, + indent_chars=self.indent_chars, + tab_stop_width=self.tab_stop_width, + cloned_from=cloned_from, + filename=fname) + if cloned_from is None: + editor.set_text(txt) + editor.document().setModified(False) + finfo.text_changed_at.connect( + lambda fname, position: + self.text_changed_at.emit(fname, position)) + editor.sig_cursor_position_changed.connect( + self.editor_cursor_position_changed) + editor.textChanged.connect(self.start_stop_analysis_timer) + editor.modificationChanged.connect( + lambda state: self.modification_changed(state, + editor_id=id(editor))) + editor.focus_in.connect(self.focus_changed) + editor.zoom_in.connect(lambda: self.zoom_in.emit()) + editor.zoom_out.connect(lambda: self.zoom_out.emit()) + editor.zoom_reset.connect(lambda: self.zoom_reset.emit()) + if self.outlineexplorer is not None: + # Removing editor reference from outline explorer settings: + editor.destroyed.connect(lambda obj=editor: + self.outlineexplorer.remove_editor(obj)) + + self.find_widget.set_editor(editor) + + self.refresh_file_dependent_actions.emit() + self.modification_changed(index=self.data.index(finfo)) + + return finfo + + def editor_cursor_position_changed(self, line, index): + """Cursor position of one of the editor in the stack has changed""" + self.sig_editor_cursor_position_changed.emit(line, index) + + def send_to_help(self, qstr1, qstr2=None, qstr3=None, qstr4=None, + force=False): + """qstr1: obj_text, qstr2: argpspec, qstr3: note, qstr4: doc_text""" + if not force and not self.help_enabled: + return + if self.help is not None \ + and (force or self.help.dockwidget.isVisible()): + # Help plugin exists and is visible + if qstr4 is None: + self.help.set_object_text(qstr1, ignore_unknown=True, + force_refresh=force) + else: + objtxt = to_text_string(qstr1) + name = objtxt.split('.')[-1] + argspec = to_text_string(qstr2) + note = to_text_string(qstr3) + docstring = to_text_string(qstr4) + doc = {'obj_text': objtxt, 'name': name, 'argspec': argspec, + 'note': note, 'docstring': docstring} + self.help.set_editor_doc(doc, force_refresh=force) + editor = self.get_current_editor() + editor.setFocus() + + def new(self, filename, encoding, text, default_content=False): + """ + Create new filename with *encoding* and *text* + """ + finfo = self.create_new_editor(filename, encoding, text, + set_current=False, new=True) + finfo.editor.set_cursor_position('eof') + finfo.editor.insert_text(os.linesep) + if default_content: + finfo.default = True + finfo.editor.document().setModified(False) + return finfo + + def load(self, filename, set_current=True): + """ + Load filename, create an editor instance and return it + *Warning* This is loading file, creating editor but not executing + the source code analysis -- the analysis must be done by the editor + plugin (in case multiple editorstack instances are handled) + """ + filename = osp.abspath(to_text_string(filename)) + self.starting_long_process.emit(_("Loading %s...") % filename) + text, enc = encoding.read(filename) + finfo = self.create_new_editor(filename, enc, text, set_current) + index = self.data.index(finfo) + self._refresh_outlineexplorer(index, update=True) + self.ending_long_process.emit("") + if self.isVisible() and self.checkeolchars_enabled \ + and sourcecode.has_mixed_eol_chars(text): + name = osp.basename(filename) + QMessageBox.warning(self, self.title, + _("%s contains mixed end-of-line " + "characters.
    Spyder will fix this " + "automatically.") % name, + QMessageBox.Ok) + self.set_os_eol_chars(index) + self.is_analysis_done = False + return finfo + + def set_os_eol_chars(self, index=None): + if index is None: + index = self.get_stack_index() + finfo = self.data[index] + eol_chars = sourcecode.get_eol_chars_from_os_name(os.name) + finfo.editor.set_eol_chars(eol_chars) + finfo.editor.document().setModified(True) + + def remove_trailing_spaces(self, index=None): + """Remove trailing spaces""" + if index is None: + index = self.get_stack_index() + finfo = self.data[index] + finfo.editor.remove_trailing_spaces() + + def fix_indentation(self, index=None): + """Replace tab characters by spaces""" + if index is None: + index = self.get_stack_index() + finfo = self.data[index] + finfo.editor.fix_indentation() + + #------ Run + def run_selection(self): + """ + Run selected text or current line in console. + + If some text is selected, then execute that text in console. + + If no text is selected, then execute current line, unless current line + is empty. Then, advance cursor to next line. If cursor is on last line + and that line is not empty, then add a new blank line and move the + cursor there. If cursor is on last line and that line is empty, then do + not move cursor. + """ + text = self.get_current_editor().get_selection_as_executable_code() + if text: + self.exec_in_extconsole.emit(text, self.focus_to_editor) + return + editor = self.get_current_editor() + line = editor.get_current_line() + text = line.lstrip() + if text: + self.exec_in_extconsole.emit(text, self.focus_to_editor) + if editor.is_cursor_on_last_line() and text: + editor.append(editor.get_line_separator()) + editor.move_cursor_to_next('line', 'down') + + def run_cell(self): + """Run current cell""" + text = self.get_current_editor().get_cell_as_executable_code() + finfo = self.get_current_finfo() + if finfo.editor.is_python() and text: + self.exec_in_extconsole.emit(text, self.focus_to_editor) + + def run_cell_and_advance(self): + """Run current cell and advance to the next one""" + self.run_cell() + if self.focus_to_editor: + self.get_current_editor().go_to_next_cell() + else: + term = QApplication.focusWidget() + self.get_current_editor().go_to_next_cell() + term.setFocus() + term = QApplication.focusWidget() + self.get_current_editor().go_to_next_cell() + term.setFocus() + + #------ Drag and drop + def dragEnterEvent(self, event): + """Reimplement Qt method + Inform Qt about the types of data that the widget accepts""" + source = event.mimeData() + # The second check is necessary on Windows, where source.hasUrls() + # can return True but source.urls() is [] + if source.hasUrls() and source.urls(): + all_urls = mimedata2url(source) + text = [encoding.is_text_file(url) for url in all_urls] + if any(text): + event.acceptProposedAction() + else: + event.ignore() + elif source.hasText(): + event.acceptProposedAction() + elif os.name == 'nt': + # This covers cases like dragging from compressed files, + # which can be opened by the Editor if they are plain + # text, but doesn't come with url info. + # Fixes Issue 2032 + event.acceptProposedAction() + else: + event.ignore() + + def dropEvent(self, event): + """Reimplement Qt method + Unpack dropped data and handle it""" + source = event.mimeData() + if source.hasUrls(): + files = mimedata2url(source) + files = [f for f in files if encoding.is_text_file(f)] + files = set(files or []) + for fname in files: + self.plugin_load.emit(fname) + elif source.hasText(): + editor = self.get_current_editor() + if editor is not None: + editor.insert_text( source.text() ) + event.acceptProposedAction() + + +class EditorSplitter(QSplitter): + def __init__(self, parent, plugin, menu_actions, first=False, + register_editorstack_cb=None, unregister_editorstack_cb=None): + QSplitter.__init__(self, parent) + self.setAttribute(Qt.WA_DeleteOnClose) + self.setChildrenCollapsible(False) + + self.toolbar_list = None + self.menu_list = None + + self.plugin = plugin + + if register_editorstack_cb is None: + register_editorstack_cb = self.plugin.register_editorstack + self.register_editorstack_cb = register_editorstack_cb + if unregister_editorstack_cb is None: + unregister_editorstack_cb = self.plugin.unregister_editorstack + self.unregister_editorstack_cb = unregister_editorstack_cb + + self.menu_actions = menu_actions + self.editorstack = EditorStack(self, menu_actions) + self.register_editorstack_cb(self.editorstack) + if not first: + self.plugin.clone_editorstack(editorstack=self.editorstack) + self.editorstack.destroyed.connect(lambda: self.editorstack_closed()) + self.editorstack.split_vertically.connect( + lambda: self.split(orientation=Qt.Vertical)) + self.editorstack.split_horizontally.connect( + lambda: self.split(orientation=Qt.Horizontal)) + self.addWidget(self.editorstack) + + def closeEvent(self, event): + QSplitter.closeEvent(self, event) + if is_pyqt46: + self.destroyed.emit() + + def __give_focus_to_remaining_editor(self): + focus_widget = self.plugin.get_focus_widget() + if focus_widget is not None: + focus_widget.setFocus() + + def editorstack_closed(self): + if DEBUG_EDITOR: + print("method 'editorstack_closed':", file=STDOUT) + print(" self :", self, file=STDOUT) +# print >>STDOUT, " sender:", self.sender() + self.unregister_editorstack_cb(self.editorstack) + self.editorstack = None + try: + close_splitter = self.count() == 1 + except RuntimeError: + # editorsplitter has been destroyed (happens when closing a + # EditorMainWindow instance) + return + if close_splitter: + # editorstack just closed was the last widget in this QSplitter + self.close() + return + self.__give_focus_to_remaining_editor() + + def editorsplitter_closed(self): + if DEBUG_EDITOR: + print("method 'editorsplitter_closed':", file=STDOUT) + print(" self :", self, file=STDOUT) +# print >>STDOUT, " sender:", self.sender() + try: + close_splitter = self.count() == 1 and self.editorstack is None + except RuntimeError: + # editorsplitter has been destroyed (happens when closing a + # EditorMainWindow instance) + return + if close_splitter: + # editorsplitter just closed was the last widget in this QSplitter + self.close() + return + elif self.count() == 2 and self.editorstack: + # back to the initial state: a single editorstack instance, + # as a single widget in this QSplitter: orientation may be changed + self.editorstack.reset_orientation() + self.__give_focus_to_remaining_editor() + + def split(self, orientation=Qt.Vertical): + self.setOrientation(orientation) + self.editorstack.set_orientation(orientation) + editorsplitter = EditorSplitter(self.parent(), self.plugin, + self.menu_actions, + register_editorstack_cb=self.register_editorstack_cb, + unregister_editorstack_cb=self.unregister_editorstack_cb) + self.addWidget(editorsplitter) + editorsplitter.destroyed.connect(lambda: self.editorsplitter_closed()) + current_editor = editorsplitter.editorstack.get_current_editor() + if current_editor is not None: + current_editor.setFocus() + + def iter_editorstacks(self): + editorstacks = [(self.widget(0), self.orientation())] + if self.count() > 1: + editorsplitter = self.widget(1) + editorstacks += editorsplitter.iter_editorstacks() + return editorstacks + + def get_layout_settings(self): + """Return layout state""" + splitsettings = [] + for editorstack, orientation in self.iter_editorstacks(): + clines = [finfo.editor.get_cursor_line_number() + for finfo in editorstack.data] + cfname = editorstack.get_current_filename() + splitsettings.append((orientation == Qt.Vertical, cfname, clines)) + return dict(hexstate=qbytearray_to_str(self.saveState()), + sizes=self.sizes(), splitsettings=splitsettings) + + def set_layout_settings(self, settings): + """Restore layout state""" + splitsettings = settings.get('splitsettings') + if splitsettings is None: + return + splitter = self + editor = None + for index, (is_vertical, cfname, clines) in enumerate(splitsettings): + if index > 0: + splitter.split(Qt.Vertical if is_vertical else Qt.Horizontal) + splitter = splitter.widget(1) + editorstack = splitter.widget(0) + for index, finfo in enumerate(editorstack.data): + editor = finfo.editor + # FIXME: Temporal fix + try: + editor.go_to_line(clines[index]) + except IndexError: + pass + editorstack.set_current_filename(cfname) + hexstate = settings.get('hexstate') + if hexstate is not None: + self.restoreState( QByteArray().fromHex( + str(hexstate).encode('utf-8')) ) + sizes = settings.get('sizes') + if sizes is not None: + self.setSizes(sizes) + if editor is not None: + editor.clearFocus() + editor.setFocus() + + +class EditorWidget(QSplitter): + def __init__(self, parent, plugin, menu_actions, show_fullpath, + fullpath_sorting, show_all_files, show_comments): + QSplitter.__init__(self, parent) + self.setAttribute(Qt.WA_DeleteOnClose) + + statusbar = parent.statusBar() # Create a status bar + self.readwrite_status = ReadWriteStatus(self, statusbar) + self.eol_status = EOLStatus(self, statusbar) + self.encoding_status = EncodingStatus(self, statusbar) + self.cursorpos_status = CursorPositionStatus(self, statusbar) + + self.editorstacks = [] + + self.plugin = plugin + + self.find_widget = FindReplace(self, enable_replace=True) + self.plugin.register_widget_shortcuts(self.find_widget) + self.find_widget.hide() + self.outlineexplorer = OutlineExplorerWidget(self, + show_fullpath=show_fullpath, + fullpath_sorting=fullpath_sorting, + show_all_files=show_all_files, + show_comments=show_comments) + self.outlineexplorer.edit_goto.connect( + lambda filenames, goto, word: + plugin.load(filenames=filenames, goto=goto, word=word, + editorwindow=self.parent())) + + editor_widgets = QWidget(self) + editor_layout = QVBoxLayout() + editor_layout.setContentsMargins(0, 0, 0, 0) + editor_widgets.setLayout(editor_layout) + editorsplitter = EditorSplitter(self, plugin, menu_actions, + register_editorstack_cb=self.register_editorstack, + unregister_editorstack_cb=self.unregister_editorstack) + self.editorsplitter = editorsplitter + editor_layout.addWidget(editorsplitter) + editor_layout.addWidget(self.find_widget) + + splitter = QSplitter(self) + splitter.setContentsMargins(0, 0, 0, 0) + splitter.addWidget(editor_widgets) + splitter.addWidget(self.outlineexplorer) + splitter.setStretchFactor(0, 5) + splitter.setStretchFactor(1, 1) + + # Refreshing outline explorer + editorsplitter.editorstack.initialize_outlineexplorer() + + def register_editorstack(self, editorstack): + self.editorstacks.append(editorstack) + if DEBUG_EDITOR: + print("EditorWidget.register_editorstack:", editorstack, file=STDOUT) + self.__print_editorstacks() + self.plugin.last_focus_editorstack[self.parent()] = editorstack + editorstack.set_closable( len(self.editorstacks) > 1 ) + editorstack.set_outlineexplorer(self.outlineexplorer) + editorstack.set_find_widget(self.find_widget) + editorstack.reset_statusbar.connect(self.readwrite_status.hide) + editorstack.reset_statusbar.connect(self.encoding_status.hide) + editorstack.reset_statusbar.connect(self.cursorpos_status.hide) + editorstack.readonly_changed.connect( + self.readwrite_status.readonly_changed) + editorstack.encoding_changed.connect( + self.encoding_status.encoding_changed) + editorstack.sig_editor_cursor_position_changed.connect( + self.cursorpos_status.cursor_position_changed) + editorstack.refresh_eol_chars.connect(self.eol_status.eol_changed) + self.plugin.register_editorstack(editorstack) + oe_btn = create_toolbutton(self) + oe_btn.setDefaultAction(self.outlineexplorer.visibility_action) + editorstack.add_corner_widgets_to_tabbar([5, oe_btn]) + + def __print_editorstacks(self): + print("%d editorstack(s) in editorwidget:" \ + % len(self.editorstacks), file=STDOUT) + for edst in self.editorstacks: + print(" ", edst, file=STDOUT) + + def unregister_editorstack(self, editorstack): + if DEBUG_EDITOR: + print("EditorWidget.unregister_editorstack:", editorstack, file=STDOUT) + self.plugin.unregister_editorstack(editorstack) + self.editorstacks.pop(self.editorstacks.index(editorstack)) + if DEBUG_EDITOR: + self.__print_editorstacks() + + +class EditorMainWindow(QMainWindow): + def __init__(self, plugin, menu_actions, toolbar_list, menu_list, + show_fullpath, fullpath_sorting, show_all_files, + show_comments): + QMainWindow.__init__(self) + self.setAttribute(Qt.WA_DeleteOnClose) + + self.window_size = None + + self.editorwidget = EditorWidget(self, plugin, menu_actions, + show_fullpath, fullpath_sorting, + show_all_files, show_comments) + self.setCentralWidget(self.editorwidget) + + # Give focus to current editor to update/show all status bar widgets + editorstack = self.editorwidget.editorsplitter.editorstack + editor = editorstack.get_current_editor() + if editor is not None: + editor.setFocus() + + self.setWindowTitle("Spyder - %s" % plugin.windowTitle()) + self.setWindowIcon(plugin.windowIcon()) + + if toolbar_list: + toolbars = [] + for title, actions in toolbar_list: + toolbar = self.addToolBar(title) + toolbar.setObjectName(str(id(toolbar))) + add_actions(toolbar, actions) + toolbars.append(toolbar) + if menu_list: + quit_action = create_action(self, _("Close window"), + icon="close_panel.png", + tip=_("Close this window"), + triggered=self.close) + menus = [] + for index, (title, actions) in enumerate(menu_list): + menu = self.menuBar().addMenu(title) + if index == 0: + # File menu + add_actions(menu, actions+[None, quit_action]) + else: + add_actions(menu, actions) + menus.append(menu) + + def resizeEvent(self, event): + """Reimplement Qt method""" + if not self.isMaximized() and not self.isFullScreen(): + self.window_size = self.size() + QMainWindow.resizeEvent(self, event) + + def closeEvent(self, event): + """Reimplement Qt method""" + QMainWindow.closeEvent(self, event) + if is_pyqt46: + self.destroyed.emit() + for editorstack in self.editorwidget.editorstacks[:]: + if DEBUG_EDITOR: + print("--> destroy_editorstack:", editorstack, file=STDOUT) + editorstack.destroyed.emit() + + def get_layout_settings(self): + """Return layout state""" + splitsettings = self.editorwidget.editorsplitter.get_layout_settings() + return dict(size=(self.window_size.width(), self.window_size.height()), + pos=(self.pos().x(), self.pos().y()), + is_maximized=self.isMaximized(), + is_fullscreen=self.isFullScreen(), + hexstate=qbytearray_to_str(self.saveState()), + splitsettings=splitsettings) + + def set_layout_settings(self, settings): + """Restore layout state""" + size = settings.get('size') + if size is not None: + self.resize( QSize(*size) ) + self.window_size = self.size() + pos = settings.get('pos') + if pos is not None: + self.move( QPoint(*pos) ) + hexstate = settings.get('hexstate') + if hexstate is not None: + self.restoreState( QByteArray().fromHex( + str(hexstate).encode('utf-8')) ) + if settings.get('is_maximized'): + self.setWindowState(Qt.WindowMaximized) + if settings.get('is_fullscreen'): + self.setWindowState(Qt.WindowFullScreen) + splitsettings = settings.get('splitsettings') + if splitsettings is not None: + self.editorwidget.editorsplitter.set_layout_settings(splitsettings) + + +class EditorPluginExample(QSplitter): + def __init__(self): + QSplitter.__init__(self) + + menu_actions = [] + + self.editorstacks = [] + self.editorwindows = [] + + self.last_focus_editorstack = {} # fake + + self.find_widget = FindReplace(self, enable_replace=True) + self.outlineexplorer = OutlineExplorerWidget(self, show_fullpath=False, + show_all_files=False) + self.outlineexplorer.edit_goto.connect(self.go_to_file) + self.editor_splitter = EditorSplitter(self, self, menu_actions, + first=True) + + editor_widgets = QWidget(self) + editor_layout = QVBoxLayout() + editor_layout.setContentsMargins(0, 0, 0, 0) + editor_widgets.setLayout(editor_layout) + editor_layout.addWidget(self.editor_splitter) + editor_layout.addWidget(self.find_widget) + + self.setContentsMargins(0, 0, 0, 0) + self.addWidget(editor_widgets) + self.addWidget(self.outlineexplorer) + + self.setStretchFactor(0, 5) + self.setStretchFactor(1, 1) + + self.menu_actions = menu_actions + self.toolbar_list = None + self.menu_list = None + self.setup_window([], []) + + def go_to_file(self, fname, lineno, text): + editorstack = self.editorstacks[0] + editorstack.set_current_filename(to_text_string(fname)) + editor = editorstack.get_current_editor() + editor.go_to_line(lineno, word=text) + + def closeEvent(self, event): + for win in self.editorwindows[:]: + win.close() + if DEBUG_EDITOR: + print(len(self.editorwindows), ":", self.editorwindows, file=STDOUT) + print(len(self.editorstacks), ":", self.editorstacks, file=STDOUT) + + event.accept() + + def load(self, fname): + QApplication.processEvents() + editorstack = self.editorstacks[0] + editorstack.load(fname) + editorstack.analyze_script() + + def register_editorstack(self, editorstack): + if DEBUG_EDITOR: + print("FakePlugin.register_editorstack:", editorstack, file=STDOUT) + self.editorstacks.append(editorstack) + if self.isAncestorOf(editorstack): + # editorstack is a child of the Editor plugin + editorstack.set_fullpath_sorting_enabled(True) + editorstack.set_closable( len(self.editorstacks) > 1 ) + editorstack.set_outlineexplorer(self.outlineexplorer) + editorstack.set_find_widget(self.find_widget) + oe_btn = create_toolbutton(self) + oe_btn.setDefaultAction(self.outlineexplorer.visibility_action) + editorstack.add_corner_widgets_to_tabbar([5, oe_btn]) + + action = QAction(self) + editorstack.set_io_actions(action, action, action, action) + font = QFont("Courier New") + font.setPointSize(10) + editorstack.set_default_font(font, color_scheme='Spyder') + + editorstack.sig_close_file.connect(self.close_file_in_all_editorstacks) + editorstack.file_saved.connect(self.file_saved_in_editorstack) + editorstack.file_renamed_in_data.connect( + self.file_renamed_in_data_in_editorstack) + editorstack.create_new_window.connect(self.create_new_window) + editorstack.plugin_load.connect(self.load) + + def unregister_editorstack(self, editorstack): + if DEBUG_EDITOR: + print("FakePlugin.unregister_editorstack:", editorstack, file=STDOUT) + self.editorstacks.pop(self.editorstacks.index(editorstack)) + + def clone_editorstack(self, editorstack): + editorstack.clone_from(self.editorstacks[0]) + + def setup_window(self, toolbar_list, menu_list): + self.toolbar_list = toolbar_list + self.menu_list = menu_list + + def create_new_window(self): + window = EditorMainWindow(self, self.menu_actions, + self.toolbar_list, self.menu_list, + show_fullpath=False, fullpath_sorting=True, + show_all_files=False, show_comments=True) + window.resize(self.size()) + window.show() + self.register_editorwindow(window) + window.destroyed.connect(lambda: self.unregister_editorwindow(window)) + + def register_editorwindow(self, window): + if DEBUG_EDITOR: + print("register_editorwindowQObject*:", window, file=STDOUT) + self.editorwindows.append(window) + + def unregister_editorwindow(self, window): + if DEBUG_EDITOR: + print("unregister_editorwindow:", window, file=STDOUT) + self.editorwindows.pop(self.editorwindows.index(window)) + + def get_focus_widget(self): + pass + + @Slot(str, int) + def close_file_in_all_editorstacks(self, editorstack_id_str, index): + for editorstack in self.editorstacks: + if str(id(editorstack)) != editorstack_id_str: + editorstack.blockSignals(True) + editorstack.close_file(index, force=True) + editorstack.blockSignals(False) + + # This method is never called in this plugin example. It's here only + # to show how to use the file_saved signal (see above). + @Slot(str, int, str) + def file_saved_in_editorstack(self, editorstack_id_str, index, filename): + """A file was saved in editorstack, this notifies others""" + for editorstack in self.editorstacks: + if str(id(editorstack)) != editorstack_id_str: + editorstack.file_saved_in_other_editorstack(index, filename) + + # This method is never called in this plugin example. It's here only + # to show how to use the file_saved signal (see above). + @Slot(str, int, str) + def file_renamed_in_data_in_editorstack(self, editorstack_id_str, + index, filename): + """A file was renamed in data in editorstack, this notifies others""" + for editorstack in self.editorstacks: + if str(id(editorstack)) != editorstack_id_str: + editorstack.rename_in_data(index, filename) + + def register_widget_shortcuts(self, widget): + """Fake!""" + pass + + +def test(): + from spyder.utils.qthelpers import qapplication + from spyder.config.base import get_module_path + from spyder.utils.introspection.manager import IntrospectionManager + + cur_dir = osp.join(get_module_path('spyder'), 'widgets') + app = qapplication(test_time=8) + introspector = IntrospectionManager() + + test = EditorPluginExample() + test.resize(900, 700) + test.show() + + editorstack = test.editor_splitter.editorstack + editorstack.set_introspector(introspector) + introspector.set_editor_widget(editorstack) + + import time + t0 = time.time() + test.load(osp.join(cur_dir, "editor.py")) + test.load(osp.join(cur_dir, "explorer.py")) + test.load(osp.join(cur_dir, "variableexplorer", "collectionseditor.py")) + test.load(osp.join(cur_dir, "sourcecode", "codeeditor.py")) + print("Elapsed time: %.3f s" % (time.time()-t0)) + + sys.exit(app.exec_()) + + +if __name__ == "__main__": + test() diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/widgets/editortools.py spyder-3.0.2+dfsg1/spyder/widgets/editortools.py --- spyder-2.3.8+dfsg1/spyder/widgets/editortools.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/widgets/editortools.py 2016-11-16 03:30:06.000000000 +0100 @@ -1,27 +1,28 @@ # -*- coding: utf-8 -*- # -# Copyright © 2010 Pierre Raybaut +# Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) +# (see spyder/__init__.py for details) """Editor tools: outline explorer, etc.""" +# Standard library imports from __future__ import print_function - -import re import os.path as osp +import re -from spyderlib.qt.QtGui import (QWidget, QTreeWidgetItem, QHBoxLayout, - QVBoxLayout) -from spyderlib.qt.QtCore import Qt, SIGNAL -from spyderlib.qt.compat import from_qvariant - -# Local import -from spyderlib.baseconfig import _, STDOUT -from spyderlib.utils.qthelpers import (get_icon, create_action, - create_toolbutton, set_item_user_text) -from spyderlib.widgets.onecolumntree import OneColumnTree -from spyderlib.py3compat import to_text_string +# Third party imports +from qtpy.compat import from_qvariant +from qtpy.QtCore import Qt, Signal, Slot +from qtpy.QtWidgets import QHBoxLayout, QTreeWidgetItem, QVBoxLayout, QWidget + +# Local imports +from spyder.config.base import _, STDOUT +from spyder.py3compat import to_text_string +from spyder.utils import icon_manager as ima +from spyder.utils.qthelpers import (create_action, create_toolbutton, + set_item_user_text) +from spyder.widgets.onecolumntree import OneColumnTree #=============================================================================== @@ -51,7 +52,7 @@ def __init__(self, path, treewidget): QTreeWidgetItem.__init__(self, treewidget, QTreeWidgetItem.Type) self.path = path - self.setIcon(0, get_icon('python.png')) + self.setIcon(0, ima.icon('python')) self.setToolTip(0, path) set_item_user_text(self, path) @@ -86,15 +87,15 @@ set_item_user_text(self, parent_text+'/'+name) self.line = line - def set_icon(self, icon_name): - self.setIcon(0, get_icon(icon_name)) + def set_icon(self, icon): + self.setIcon(0, icon) def setup(self): self.setToolTip(0, _("Line %s") % str(self.line)) class ClassItem(TreeItem): def setup(self): - self.set_icon('class.png') + self.set_icon(ima.icon('class')) self.setToolTip(0, _("Class defined at line %s") % str(self.line)) class FunctionItem(TreeItem): @@ -106,13 +107,13 @@ self.setToolTip(0, _("Method defined at line %s") % str(self.line)) name = to_text_string(self.text(0)) if name.startswith('__'): - self.set_icon('private2.png') + self.set_icon(ima.icon('private2')) elif name.startswith('_'): - self.set_icon('private1.png') + self.set_icon(ima.icon('private1')) else: - self.set_icon('method.png') + self.set_icon(ima.icon('method')) else: - self.set_icon('function.png') + self.set_icon(ima.icon('function')) self.setToolTip(0, _("Function defined at line %s" ) % str(self.line)) @@ -122,7 +123,7 @@ TreeItem.__init__(self, name, line, parent, preceding) def setup(self): - self.set_icon('blockcomment.png') + self.set_icon(ima.icon('blockcomment')) font = self.font(0) font.setItalic(True) self.setFont(0, font) @@ -141,7 +142,7 @@ TreeItem.__init__(self, name, line, parent, preceding) def setup(self): - self.set_icon('cell.png') + self.set_icon(ima.icon('cell')) font = self.font(0) font.setItalic(True) self.setFont(0, font) @@ -199,7 +200,7 @@ def get_actions_from_items(self, items): """Reimplemented OneColumnTree method""" fromcursor_act = create_action(self, text=_('Go to cursor position'), - icon=get_icon('fromcursor.png'), + icon=ima.icon('fromcursor'), triggered=self.go_to_cursor_position) fullpath_act = create_action(self, text=_( 'Show absolute path'), toggled=self.toggle_fullpath_mode) @@ -212,7 +213,8 @@ comment_act.setChecked(self.show_comments) actions = [fullpath_act, allfiles_act, comment_act, fromcursor_act] return actions - + + @Slot(bool) def toggle_fullpath_mode(self, state): self.show_fullpath = state self.setTextElideMode(Qt.ElideMiddle if state else Qt.ElideRight) @@ -226,14 +228,16 @@ """ for _it in self.get_top_level_items(): _it.setHidden(_it is not item and not self.show_all_files) - + + @Slot(bool) def toggle_show_all_files(self, state): self.show_all_files = state if self.current_editor is not None: editor_id = self.editor_ids[self.current_editor] item = self.editor_items[editor_id] self.__hide_or_show_root_items(item) - + + @Slot(bool) def toggle_show_comments(self, state): self.show_comments = state self.update_all() @@ -241,7 +245,8 @@ def set_fullpath_sorting(self, state): self.fullpath_sorting = state self.__sort_toplevel_items() - + + @Slot() def go_to_cursor_position(self): if self.current_editor is not None: line = self.current_editor.get_cursor_line_number() @@ -464,10 +469,9 @@ root_item = self.get_root_item(item) self.freeze = True if line: - self.parent().emit(SIGNAL("edit_goto(QString,int,QString)"), - root_item.path, line, item.text(0)) + self.parent().edit_goto.emit(root_item.path, line, item.text(0)) else: - self.parent().emit(SIGNAL("edit(QString)"), root_item.path) + self.parent().edit.emit(root_item.path) self.freeze = False parent = self.current_editor.parent() for editor_id, i_item in list(self.editor_items.items()): @@ -487,13 +491,11 @@ class OutlineExplorerWidget(QWidget): - """ - Class browser + """Class browser""" + edit_goto = Signal(str, int, str) + edit = Signal(str) + outlineexplorer_is_visible = Signal() - Signals: - SIGNAL("edit_goto(QString,int,QString)") - SIGNAL("edit(QString)") - """ def __init__(self, parent=None, show_fullpath=True, fullpath_sorting=True, show_all_files=True, show_comments=True): QWidget.__init__(self, parent) @@ -520,7 +522,8 @@ layout.addLayout(btn_layout) layout.addWidget(self.treewidget) self.setLayout(layout) - + + @Slot(bool) def toggle_visibility(self, state): self.setVisible(state) current_editor = self.treewidget.current_editor @@ -528,11 +531,11 @@ current_editor.clearFocus() current_editor.setFocus() if state: - self.emit(SIGNAL("outlineexplorer_is_visible()")) + self.outlineexplorer_is_visible.emit() def setup_buttons(self): fromcursor_btn = create_toolbutton(self, - icon=get_icon("fromcursor.png"), + icon=ima.icon('fromcursor'), tip=_('Go to cursor position'), triggered=self.treewidget.go_to_cursor_position) collapse_btn = create_toolbutton(self) diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/widgets/explorer.py spyder-3.0.2+dfsg1/spyder/widgets/explorer.py --- spyder-2.3.8+dfsg1/spyder/widgets/explorer.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/widgets/explorer.py 2016-11-17 04:39:40.000000000 +0100 @@ -1,1107 +1,1232 @@ -# -*- coding: utf-8 -*- -# -# Copyright © 2009-2010 Pierre Raybaut -# Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) - -"""Files and Directories Explorer""" - -# pylint: disable=C0103 -# pylint: disable=R0903 -# pylint: disable=R0911 -# pylint: disable=R0201 - -from __future__ import with_statement - -from spyderlib.qt.QtGui import (QVBoxLayout, QLabel, QHBoxLayout, QInputDialog, - QFileSystemModel, QMenu, QWidget, QToolButton, - QLineEdit, QMessageBox, QToolBar, QTreeView, - QDrag, QSortFilterProxyModel) -from spyderlib.qt.QtCore import (Qt, SIGNAL, QMimeData, QSize, QDir, QUrl, - Signal, QTimer) -from spyderlib.qt.compat import getsavefilename, getexistingdirectory - -import os -import sys -import re -import os.path as osp -import shutil - -# Local imports -from spyderlib.utils.qthelpers import (get_icon, create_action, add_actions, - file_uri, get_std_icon) -from spyderlib.utils import misc, encoding, programs, vcs -from spyderlib.baseconfig import _ -from spyderlib.py3compat import (to_text_string, to_binary_string, getcwd, - str_lower) - -try: - from IPython.nbconvert import PythonExporter as nbexporter -except: - nbexporter = None # analysis:ignore - - -def fixpath(path): - """Normalize path fixing case, making absolute and removing symlinks""" - norm = osp.normcase if os.name == 'nt' else osp.normpath - return norm(osp.abspath(osp.realpath(path))) - - -def create_script(fname): - """Create a new Python script""" - text = os.linesep.join(["# -*- coding: utf-8 -*-", "", ""]) - encoding.write(to_text_string(text), fname, 'utf-8') - - -def listdir(path, include='.', exclude=r'\.pyc$|^\.', show_all=False, - folders_only=False): - """List files and directories""" - namelist = [] - dirlist = [to_text_string(osp.pardir)] - for item in os.listdir(to_text_string(path)): - if re.search(exclude, item) and not show_all: - continue - if osp.isdir(osp.join(path, item)): - dirlist.append(item) - elif folders_only: - continue - elif re.search(include, item) or show_all: - namelist.append(item) - return sorted(dirlist, key=str_lower) + \ - sorted(namelist, key=str_lower) - - -def has_subdirectories(path, include, exclude, show_all): - """Return True if path has subdirectories""" - try: - # > 1 because of '..' - return len( listdir(path, include, exclude, - show_all, folders_only=True) ) > 1 - except (IOError, OSError): - return False - - -class DirView(QTreeView): - """Base file/directory tree view""" - def __init__(self, parent=None): - super(DirView, self).__init__(parent) - self.name_filters = None - self.parent_widget = parent - self.show_all = None - self.menu = None - self.common_actions = None - self.__expanded_state = None - self._to_be_loaded = None - self.fsmodel = None - self.setup_fs_model() - self._scrollbar_positions = None - - #---- Model - def setup_fs_model(self): - """Setup filesystem model""" - filters = QDir.AllDirs | QDir.Files | QDir.Drives | QDir.NoDotAndDotDot - self.fsmodel = QFileSystemModel(self) - self.fsmodel.setFilter(filters) - self.fsmodel.setNameFilterDisables(False) - - def install_model(self): - """Install filesystem model""" - self.setModel(self.fsmodel) - - def setup_view(self): - """Setup view""" - self.install_model() - self.connect(self.fsmodel, SIGNAL('directoryLoaded(QString)'), - lambda: self.resizeColumnToContents(0)) - self.setAnimated(False) - self.setSortingEnabled(True) - self.sortByColumn(0, Qt.AscendingOrder) - - def set_name_filters(self, name_filters): - """Set name filters""" - self.name_filters = name_filters - self.fsmodel.setNameFilters(name_filters) - - def set_show_all(self, state): - """Toggle 'show all files' state""" - if state: - self.fsmodel.setNameFilters([]) - else: - self.fsmodel.setNameFilters(self.name_filters) - - def get_filename(self, index): - """Return filename associated with *index*""" - if index: - return osp.normpath(to_text_string(self.fsmodel.filePath(index))) - - def get_index(self, filename): - """Return index associated with filename""" - return self.fsmodel.index(filename) - - def get_selected_filenames(self): - """Return selected filenames""" - if self.selectionMode() == self.ExtendedSelection: - return [self.get_filename(idx) for idx in self.selectedIndexes()] - else: - return [self.get_filename(self.currentIndex())] - - def get_dirname(self, index): - """Return dirname associated with *index*""" - fname = self.get_filename(index) - if fname: - if osp.isdir(fname): - return fname - else: - return osp.dirname(fname) - - #---- Tree view widget - def setup(self, name_filters=['*.py', '*.pyw'], show_all=False): - """Setup tree widget""" - self.setup_view() - - self.set_name_filters(name_filters) - self.show_all = show_all - - # Setup context menu - self.menu = QMenu(self) - self.common_actions = self.setup_common_actions() - - #---- Context menu - def setup_common_actions(self): - """Setup context menu common actions""" - # Filters - filters_action = create_action(self, _("Edit filename filters..."), - None, get_icon('filter.png'), - triggered=self.edit_filter) - # Show all files - all_action = create_action(self, _("Show all files"), - toggled=self.toggle_all) - all_action.setChecked(self.show_all) - self.toggle_all(self.show_all) - - return [filters_action, all_action] - - def edit_filter(self): - """Edit name filters""" - filters, valid = QInputDialog.getText(self, _('Edit filename filters'), - _('Name filters:'), - QLineEdit.Normal, - ", ".join(self.name_filters)) - if valid: - filters = [f.strip() for f in to_text_string(filters).split(',')] - self.parent_widget.sig_option_changed.emit('name_filters', filters) - self.set_name_filters(filters) - - def toggle_all(self, checked): - """Toggle all files mode""" - self.parent_widget.sig_option_changed.emit('show_all', checked) - self.show_all = checked - self.set_show_all(checked) - - def create_file_new_actions(self, fnames): - """Return actions for submenu 'New...'""" - if not fnames: - return [] - new_file_act = create_action(self, _("File..."), icon='filenew.png', - triggered=lambda: - self.new_file(fnames[-1])) - new_module_act = create_action(self, _("Module..."), icon='py.png', - triggered=lambda: - self.new_module(fnames[-1])) - new_folder_act = create_action(self, _("Folder..."), - icon='folder_new.png', - triggered=lambda: - self.new_folder(fnames[-1])) - new_package_act = create_action(self, _("Package..."), - icon=get_icon('package_collapsed.png'), - triggered=lambda: - self.new_package(fnames[-1])) - return [new_file_act, new_folder_act, None, - new_module_act, new_package_act] - - def create_file_import_actions(self, fnames): - """Return actions for submenu 'Import...'""" - return [] - - def create_file_manage_actions(self, fnames): - """Return file management actions""" - only_files = all([osp.isfile(_fn) for _fn in fnames]) - only_modules = all([osp.splitext(_fn)[1] in ('.py', '.pyw', '.ipy') - for _fn in fnames]) - only_notebooks = all([osp.splitext(_fn)[1] == '.ipynb' - for _fn in fnames]) - only_valid = all([encoding.is_text_file(_fn) for _fn in fnames]) - run_action = create_action(self, _("Run"), icon="run_small.png", - triggered=self.run) - edit_action = create_action(self, _("Edit"), icon="edit.png", - triggered=self.clicked) - move_action = create_action(self, _("Move..."), - icon="move.png", - triggered=self.move) - delete_action = create_action(self, _("Delete..."), - icon="delete.png", - triggered=self.delete) - rename_action = create_action(self, _("Rename..."), - icon="rename.png", - triggered=self.rename) - open_action = create_action(self, _("Open"), triggered=self.open) - ipynb_convert_action = create_action(self, _("Convert to Python script"), - icon="python.png", - triggered=self.convert) - - actions = [] - if only_modules: - actions.append(run_action) - if only_valid and only_files: - actions.append(edit_action) - else: - actions.append(open_action) - actions += [delete_action, rename_action] - basedir = fixpath(osp.dirname(fnames[0])) - if all([fixpath(osp.dirname(_fn)) == basedir for _fn in fnames]): - actions.append(move_action) - actions += [None] - if only_notebooks and nbexporter is not None: - actions.append(ipynb_convert_action) - - # VCS support is quite limited for now, so we are enabling the VCS - # related actions only when a single file/folder is selected: - dirname = fnames[0] if osp.isdir(fnames[0]) else osp.dirname(fnames[0]) - if len(fnames) == 1 and vcs.is_vcs_repository(dirname): - vcs_ci = create_action(self, _("Commit"), - icon="vcs_commit.png", - triggered=lambda fnames=[dirname]: - self.vcs_command(fnames, 'commit')) - vcs_log = create_action(self, _("Browse repository"), - icon="vcs_browse.png", - triggered=lambda fnames=[dirname]: - self.vcs_command(fnames, 'browse')) - actions += [None, vcs_ci, vcs_log] - - return actions - - def create_folder_manage_actions(self, fnames): - """Return folder management actions""" - actions = [] - if os.name == 'nt': - _title = _("Open command prompt here") - else: - _title = _("Open terminal here") - action = create_action(self, _title, icon="cmdprompt.png", - triggered=lambda fnames=fnames: - self.open_terminal(fnames)) - actions.append(action) - _title = _("Open Python console here") - action = create_action(self, _title, icon="python.png", - triggered=lambda fnames=fnames: - self.open_interpreter(fnames)) - actions.append(action) - return actions - - def create_context_menu_actions(self): - """Create context menu actions""" - actions = [] - fnames = self.get_selected_filenames() - new_actions = self.create_file_new_actions(fnames) - if len(new_actions) > 1: - # Creating a submenu only if there is more than one entry - new_act_menu = QMenu(_('New'), self) - add_actions(new_act_menu, new_actions) - actions.append(new_act_menu) - else: - actions += new_actions - import_actions = self.create_file_import_actions(fnames) - if len(import_actions) > 1: - # Creating a submenu only if there is more than one entry - import_act_menu = QMenu(_('Import'), self) - add_actions(import_act_menu, import_actions) - actions.append(import_act_menu) - else: - actions += import_actions - if actions: - actions.append(None) - if fnames: - actions += self.create_file_manage_actions(fnames) - if actions: - actions.append(None) - if fnames and all([osp.isdir(_fn) for _fn in fnames]): - actions += self.create_folder_manage_actions(fnames) - if actions: - actions.append(None) - actions += self.common_actions - return actions - - def update_menu(self): - """Update context menu""" - self.menu.clear() - add_actions(self.menu, self.create_context_menu_actions()) - - #---- Events - def viewportEvent(self, event): - """Reimplement Qt method""" - - # Prevent Qt from crashing or showing warnings like: - # "QSortFilterProxyModel: index from wrong model passed to - # mapFromSource", probably due to the fact that the file system model - # is being built. See Issue 1250. - # - # This workaround was inspired by the following KDE bug: - # https://bugs.kde.org/show_bug.cgi?id=172198 - # - # Apparently, this is a bug from Qt itself. - self.executeDelayedItemsLayout() - - return QTreeView.viewportEvent(self, event) - - def contextMenuEvent(self, event): - """Override Qt method""" - self.update_menu() - self.menu.popup(event.globalPos()) - - def keyPressEvent(self, event): - """Reimplement Qt method""" - if event.key() in (Qt.Key_Enter, Qt.Key_Return): - self.clicked() - elif event.key() == Qt.Key_F2: - self.rename() - elif event.key() == Qt.Key_Delete: - self.delete() - else: - QTreeView.keyPressEvent(self, event) - - def mouseDoubleClickEvent(self, event): - """Reimplement Qt method""" - QTreeView.mouseDoubleClickEvent(self, event) - self.clicked() - - def clicked(self): - """Selected item was double-clicked or enter/return was pressed""" - fnames = self.get_selected_filenames() - for fname in fnames: - if osp.isdir(fname): - self.directory_clicked(fname) - else: - self.open([fname]) - - def directory_clicked(self, dirname): - """Directory was just clicked""" - pass - - #---- Drag - def dragEnterEvent(self, event): - """Drag and Drop - Enter event""" - event.setAccepted(event.mimeData().hasFormat("text/plain")) - - def dragMoveEvent(self, event): - """Drag and Drop - Move event""" - if (event.mimeData().hasFormat("text/plain")): - event.setDropAction(Qt.MoveAction) - event.accept() - else: - event.ignore() - - def startDrag(self, dropActions): - """Reimplement Qt Method - handle drag event""" - data = QMimeData() - data.setUrls([QUrl(fname) for fname in self.get_selected_filenames()]) - drag = QDrag(self) - drag.setMimeData(data) - drag.exec_() - - #---- File/Directory actions - def open(self, fnames=None): - """Open files with the appropriate application""" - if fnames is None: - fnames = self.get_selected_filenames() - for fname in fnames: - if osp.isfile(fname) and encoding.is_text_file(fname): - self.parent_widget.sig_open_file.emit(fname) - else: - self.open_outside_spyder([fname]) - - def open_outside_spyder(self, fnames): - """Open file outside Spyder with the appropriate application - If this does not work, opening unknown file in Spyder, as text file""" - for path in sorted(fnames): - path = file_uri(path) - ok = programs.start_file(path) - if not ok: - self.parent_widget.emit(SIGNAL("edit(QString)"), path) - - def open_terminal(self, fnames): - """Open terminal""" - for path in sorted(fnames): - self.parent_widget.emit(SIGNAL("open_terminal(QString)"), path) - - def open_interpreter(self, fnames): - """Open interpreter""" - for path in sorted(fnames): - self.parent_widget.emit(SIGNAL("open_interpreter(QString)"), path) - - def run(self, fnames=None): - """Run Python scripts""" - if fnames is None: - fnames = self.get_selected_filenames() - for fname in fnames: - self.parent_widget.emit(SIGNAL("run(QString)"), fname) - - def remove_tree(self, dirname): - """Remove whole directory tree - Reimplemented in project explorer widget""" - shutil.rmtree(dirname, onerror=misc.onerror) - - def delete_file(self, fname, multiple, yes_to_all): - """Delete file""" - if multiple: - buttons = QMessageBox.Yes|QMessageBox.YesAll| \ - QMessageBox.No|QMessageBox.Cancel - else: - buttons = QMessageBox.Yes|QMessageBox.No - if yes_to_all is None: - answer = QMessageBox.warning(self, _("Delete"), - _("Do you really want " - "to delete %s?" - ) % osp.basename(fname), buttons) - if answer == QMessageBox.No: - return yes_to_all - elif answer == QMessageBox.Cancel: - return False - elif answer == QMessageBox.YesAll: - yes_to_all = True - try: - if osp.isfile(fname): - misc.remove_file(fname) - self.parent_widget.emit(SIGNAL("removed(QString)"), - fname) - else: - self.remove_tree(fname) - self.parent_widget.emit(SIGNAL("removed_tree(QString)"), - fname) - return yes_to_all - except EnvironmentError as error: - action_str = _('delete') - QMessageBox.critical(self, _("Project Explorer"), - _("Unable to %s %s" - "

    Error message:
    %s" - ) % (action_str, fname, to_text_string(error))) - return False - - def delete(self, fnames=None): - """Delete files""" - if fnames is None: - fnames = self.get_selected_filenames() - multiple = len(fnames) > 1 - yes_to_all = None - for fname in fnames: - yes_to_all = self.delete_file(fname, multiple, yes_to_all) - if yes_to_all is not None and not yes_to_all: - # Canceled - return - - def convert_notebook(self, fname): - """Convert an IPython notebook to a Python script in editor""" - try: - script = nbexporter().from_filename(fname)[0] - except Exception as e: - QMessageBox.critical(self, _('Conversion error'), - _("It was not possible to convert this " - "notebook. The error is:\n\n") + \ - to_text_string(e)) - return - self.parent_widget.sig_new_file.emit(script) - - def convert(self, fnames=None): - """Convert IPython notebooks to Python scripts in editor""" - if fnames is None: - fnames = self.get_selected_filenames() - if not isinstance(fnames, (tuple, list)): - fnames = [fnames] - for fname in fnames: - self.convert_notebook(fname) - - def rename_file(self, fname): - """Rename file""" - path, valid = QInputDialog.getText(self, _('Rename'), - _('New name:'), QLineEdit.Normal, - osp.basename(fname)) - if valid: - path = osp.join(osp.dirname(fname), to_text_string(path)) - if path == fname: - return - if osp.exists(path): - if QMessageBox.warning(self, _("Rename"), - _("Do you really want to rename %s and " - "overwrite the existing file %s?" - ) % (osp.basename(fname), osp.basename(path)), - QMessageBox.Yes|QMessageBox.No) == QMessageBox.No: - return - try: - misc.rename_file(fname, path) - self.parent_widget.emit( \ - SIGNAL("renamed(QString,QString)"), fname, path) - return path - except EnvironmentError as error: - QMessageBox.critical(self, _("Rename"), - _("Unable to rename file %s" - "

    Error message:
    %s" - ) % (osp.basename(fname), to_text_string(error))) - - def rename(self, fnames=None): - """Rename files""" - if fnames is None: - fnames = self.get_selected_filenames() - if not isinstance(fnames, (tuple, list)): - fnames = [fnames] - for fname in fnames: - self.rename_file(fname) - - def move(self, fnames=None): - """Move files/directories""" - if fnames is None: - fnames = self.get_selected_filenames() - orig = fixpath(osp.dirname(fnames[0])) - while True: - self.parent_widget.emit(SIGNAL('redirect_stdio(bool)'), False) - folder = getexistingdirectory(self, _("Select directory"), orig) - self.parent_widget.emit(SIGNAL('redirect_stdio(bool)'), True) - if folder: - folder = fixpath(folder) - if folder != orig: - break - else: - return - for fname in fnames: - basename = osp.basename(fname) - try: - misc.move_file(fname, osp.join(folder, basename)) - except EnvironmentError as error: - QMessageBox.critical(self, _("Error"), - _("Unable to move %s" - "

    Error message:
    %s" - ) % (basename, to_text_string(error))) - - def create_new_folder(self, current_path, title, subtitle, is_package): - """Create new folder""" - if current_path is None: - current_path = '' - if osp.isfile(current_path): - current_path = osp.dirname(current_path) - name, valid = QInputDialog.getText(self, title, subtitle, - QLineEdit.Normal, "") - if valid: - dirname = osp.join(current_path, to_text_string(name)) - try: - os.mkdir(dirname) - except EnvironmentError as error: - QMessageBox.critical(self, title, - _("Unable " - "to create folder %s" - "

    Error message:
    %s" - ) % (dirname, to_text_string(error))) - finally: - if is_package: - fname = osp.join(dirname, '__init__.py') - try: - with open(fname, 'wb') as f: - f.write(to_binary_string('#')) - return dirname - except EnvironmentError as error: - QMessageBox.critical(self, title, - _("Unable " - "to create file %s" - "

    Error message:
    %s" - ) % (fname, - to_text_string(error))) - - def new_folder(self, basedir): - """New folder""" - title = _('New folder') - subtitle = _('Folder name:') - self.create_new_folder(basedir, title, subtitle, is_package=False) - - def new_package(self, basedir): - """New package""" - title = _('New package') - subtitle = _('Package name:') - self.create_new_folder(basedir, title, subtitle, is_package=True) - - def create_new_file(self, current_path, title, filters, create_func): - """Create new file - Returns True if successful""" - if current_path is None: - current_path = '' - if osp.isfile(current_path): - current_path = osp.dirname(current_path) - self.parent_widget.emit(SIGNAL('redirect_stdio(bool)'), False) - fname, _selfilter = getsavefilename(self, title, current_path, filters) - self.parent_widget.emit(SIGNAL('redirect_stdio(bool)'), True) - if fname: - try: - create_func(fname) - return fname - except EnvironmentError as error: - QMessageBox.critical(self, _("New file"), - _("Unable to create file %s" - "

    Error message:
    %s" - ) % (fname, to_text_string(error))) - - def new_file(self, basedir): - """New file""" - title = _("New file") - filters = _("All files")+" (*)" - def create_func(fname): - """File creation callback""" - if osp.splitext(fname)[1] in ('.py', '.pyw', '.ipy'): - create_script(fname) - else: - with open(fname, 'wb') as f: - f.write(to_binary_string('')) - fname = self.create_new_file(basedir, title, filters, create_func) - if fname is not None: - self.open([fname]) - - def new_module(self, basedir): - """New module""" - title = _("New module") - filters = _("Python scripts")+" (*.py *.pyw *.ipy)" - create_func = lambda fname: self.parent_widget.emit( \ - SIGNAL("create_module(QString)"), fname) - self.create_new_file(basedir, title, filters, create_func) - - #----- VCS actions - def vcs_command(self, fnames, action): - """VCS action (commit, browse)""" - try: - for path in sorted(fnames): - vcs.run_vcs_tool(path, action) - except vcs.ActionToolNotFound as error: - msg = _("For %s support, please install one of the
    " - "following tools:

    %s")\ - % (error.vcsname, ', '.join(error.tools)) - QMessageBox.critical(self, _("Error"), - _("""Unable to find external program.

    %s""") - % to_text_string(msg)) - - #----- Settings - def get_scrollbar_position(self): - """Return scrollbar positions""" - return (self.horizontalScrollBar().value(), - self.verticalScrollBar().value()) - - def set_scrollbar_position(self, position): - """Set scrollbar positions""" - # Scrollbars will be restored after the expanded state - self._scrollbar_positions = position - if self._to_be_loaded is not None and len(self._to_be_loaded) == 0: - self.restore_scrollbar_positions() - - def restore_scrollbar_positions(self): - """Restore scrollbar positions once tree is loaded""" - hor, ver = self._scrollbar_positions - self.horizontalScrollBar().setValue(hor) - self.verticalScrollBar().setValue(ver) - - def get_expanded_state(self): - """Return expanded state""" - self.save_expanded_state() - return self.__expanded_state - - def set_expanded_state(self, state): - """Set expanded state""" - self.__expanded_state = state - self.restore_expanded_state() - - def save_expanded_state(self): - """Save all items expanded state""" - model = self.model() - # If model is not installed, 'model' will be None: this happens when - # using the Project Explorer without having selected a workspace yet - if model is not None: - self.__expanded_state = [] - for idx in model.persistentIndexList(): - if self.isExpanded(idx): - self.__expanded_state.append(self.get_filename(idx)) - - def restore_directory_state(self, fname): - """Restore directory expanded state""" - root = osp.normpath(to_text_string(fname)) - if not osp.exists(root): - # Directory has been (re)moved outside Spyder - return - for basename in os.listdir(root): - path = osp.normpath(osp.join(root, basename)) - if osp.isdir(path) and path in self.__expanded_state: - self.__expanded_state.pop(self.__expanded_state.index(path)) - if self._to_be_loaded is None: - self._to_be_loaded = [] - self._to_be_loaded.append(path) - self.setExpanded(self.get_index(path), True) - if not self.__expanded_state: - self.disconnect(self.fsmodel, SIGNAL('directoryLoaded(QString)'), - self.restore_directory_state) - - def follow_directories_loaded(self, fname): - """Follow directories loaded during startup""" - if self._to_be_loaded is None: - return - path = osp.normpath(to_text_string(fname)) - if path in self._to_be_loaded: - self._to_be_loaded.remove(path) - if self._to_be_loaded is not None and len(self._to_be_loaded) == 0: - self.disconnect(self.fsmodel, SIGNAL('directoryLoaded(QString)'), - self.follow_directories_loaded) - if self._scrollbar_positions is not None: - # The tree view need some time to render branches: - QTimer.singleShot(50, self.restore_scrollbar_positions) - - def restore_expanded_state(self): - """Restore all items expanded state""" - if self.__expanded_state is not None: - # In the old project explorer, the expanded state was a dictionnary: - if isinstance(self.__expanded_state, list): - self.connect(self.fsmodel, SIGNAL('directoryLoaded(QString)'), - self.restore_directory_state) - self.connect(self.fsmodel, SIGNAL('directoryLoaded(QString)'), - self.follow_directories_loaded) - - -class ProxyModel(QSortFilterProxyModel): - """Proxy model: filters tree view""" - def __init__(self, parent): - super(ProxyModel, self).__init__(parent) - self.root_path = None - self.path_list = [] - self.setDynamicSortFilter(True) - - def setup_filter(self, root_path, path_list): - """Setup proxy model filter parameters""" - self.root_path = osp.normpath(to_text_string(root_path)) - self.path_list = [osp.normpath(to_text_string(p)) for p in path_list] - self.invalidateFilter() - - def sort(self, column, order=Qt.AscendingOrder): - """Reimplement Qt method""" - self.sourceModel().sort(column, order) - - def filterAcceptsRow(self, row, parent_index): - """Reimplement Qt method""" - if self.root_path is None: - return True - index = self.sourceModel().index(row, 0, parent_index) - path = osp.normpath(to_text_string(self.sourceModel().filePath(index))) - if self.root_path.startswith(path): - # This is necessary because parent folders need to be scanned - return True - else: - for p in self.path_list: - if path == p or path.startswith(p+os.sep): - return True - else: - return False - - -class FilteredDirView(DirView): - """Filtered file/directory tree view""" - def __init__(self, parent=None): - super(FilteredDirView, self).__init__(parent) - self.proxymodel = None - self.setup_proxy_model() - self.root_path = None - - #---- Model - def setup_proxy_model(self): - """Setup proxy model""" - self.proxymodel = ProxyModel(self) - self.proxymodel.setSourceModel(self.fsmodel) - - def install_model(self): - """Install proxy model""" - if self.root_path is not None: - self.fsmodel.setNameFilters(self.name_filters) - self.setModel(self.proxymodel) - - def set_root_path(self, root_path): - """Set root path""" - self.root_path = root_path - self.install_model() - index = self.fsmodel.setRootPath(root_path) - self.setRootIndex(self.proxymodel.mapFromSource(index)) - - def get_index(self, filename): - """Return index associated with filename""" - index = self.fsmodel.index(filename) - if index.isValid() and index.model() is self.fsmodel: - return self.proxymodel.mapFromSource(index) - - def set_folder_names(self, folder_names): - """Set folder names""" - assert self.root_path is not None - path_list = [osp.join(self.root_path, dirname) - for dirname in folder_names] - self.proxymodel.setup_filter(self.root_path, path_list) - - def get_filename(self, index): - """Return filename from index""" - if index: - path = self.fsmodel.filePath(self.proxymodel.mapToSource(index)) - return osp.normpath(to_text_string(path)) - - -class ExplorerTreeWidget(DirView): - """File/directory explorer tree widget - show_cd_only: Show current directory only - (True/False: enable/disable the option - None: enable the option and do not allow the user to disable it)""" - def __init__(self, parent=None, show_cd_only=None): - DirView.__init__(self, parent) - - self.history = [] - self.histindex = None - - self.show_cd_only = show_cd_only - self.__original_root_index = None - self.__last_folder = None - - self.menu = None - self.common_actions = None - - # Enable drag events - self.setDragEnabled(True) - - #---- Context menu - def setup_common_actions(self): - """Setup context menu common actions""" - actions = super(ExplorerTreeWidget, self).setup_common_actions() - if self.show_cd_only is None: - # Enabling the 'show current directory only' option but do not - # allow the user to disable it - self.show_cd_only = True - else: - # Show current directory only - cd_only_action = create_action(self, - _("Show current directory only"), - toggled=self.toggle_show_cd_only) - cd_only_action.setChecked(self.show_cd_only) - self.toggle_show_cd_only(self.show_cd_only) - actions.append(cd_only_action) - return actions - - def toggle_show_cd_only(self, checked): - """Toggle show current directory only mode""" - self.parent_widget.sig_option_changed.emit('show_cd_only', checked) - self.show_cd_only = checked - if checked: - if self.__last_folder is not None: - self.set_current_folder(self.__last_folder) - elif self.__original_root_index is not None: - self.setRootIndex(self.__original_root_index) - - #---- Refreshing widget - def set_current_folder(self, folder): - """Set current folder and return associated model index""" - index = self.fsmodel.setRootPath(folder) - self.__last_folder = folder - if self.show_cd_only: - if self.__original_root_index is None: - self.__original_root_index = self.rootIndex() - self.setRootIndex(index) - return index - - def refresh(self, new_path=None, force_current=False): - """Refresh widget - force=False: won't refresh widget if path has not changed""" - if new_path is None: - new_path = getcwd() - if force_current: - index = self.set_current_folder(new_path) - self.expand(index) - self.setCurrentIndex(index) - self.emit(SIGNAL("set_previous_enabled(bool)"), - self.histindex is not None and self.histindex > 0) - self.emit(SIGNAL("set_next_enabled(bool)"), - self.histindex is not None and \ - self.histindex < len(self.history)-1) - - #---- Events - def directory_clicked(self, dirname): - """Directory was just clicked""" - self.chdir(directory=dirname) - - #---- Files/Directories Actions - def go_to_parent_directory(self): - """Go to parent directory""" - self.chdir( osp.abspath(osp.join(getcwd(), os.pardir)) ) - - def go_to_previous_directory(self): - """Back to previous directory""" - self.histindex -= 1 - self.chdir(browsing_history=True) - - def go_to_next_directory(self): - """Return to next directory""" - self.histindex += 1 - self.chdir(browsing_history=True) - - def update_history(self, directory): - """Update browse history""" - directory = osp.abspath(to_text_string(directory)) - if directory in self.history: - self.histindex = self.history.index(directory) - - def chdir(self, directory=None, browsing_history=False): - """Set directory as working directory""" - if directory is not None: - directory = osp.abspath(to_text_string(directory)) - if browsing_history: - directory = self.history[self.histindex] - elif directory in self.history: - self.histindex = self.history.index(directory) - else: - if self.histindex is None: - self.history = [] - else: - self.history = self.history[:self.histindex+1] - if len(self.history) == 0 or \ - (self.history and self.history[-1] != directory): - self.history.append(directory) - self.histindex = len(self.history)-1 - directory = to_text_string(directory) - os.chdir(directory) - self.parent_widget.emit(SIGNAL("open_dir(QString)"), directory) - self.refresh(new_path=directory, force_current=True) - - -class ExplorerWidget(QWidget): - """Explorer widget""" - sig_option_changed = Signal(str, object) - sig_open_file = Signal(str) - sig_new_file = Signal(str) - - def __init__(self, parent=None, name_filters=['*.py', '*.pyw'], - show_all=False, show_cd_only=None, show_icontext=True): - QWidget.__init__(self, parent) - - self.treewidget = ExplorerTreeWidget(self, show_cd_only=show_cd_only) - self.treewidget.setup(name_filters=name_filters, show_all=show_all) - self.treewidget.chdir(getcwd()) - - icontext_action = create_action(self, _("Show icons and text"), - toggled=self.toggle_icontext) - self.treewidget.common_actions += [None, icontext_action] - - # Setup toolbar - self.toolbar = QToolBar(self) - self.toolbar.setIconSize(QSize(16, 16)) - - self.previous_action = create_action(self, text=_("Previous"), - icon=get_std_icon("ArrowBack"), - triggered=self.treewidget.go_to_previous_directory) - self.toolbar.addAction(self.previous_action) - self.previous_action.setEnabled(False) - self.connect(self.treewidget, SIGNAL("set_previous_enabled(bool)"), - self.previous_action.setEnabled) - - self.next_action = create_action(self, text=_("Next"), - icon=get_std_icon("ArrowForward"), - triggered=self.treewidget.go_to_next_directory) - self.toolbar.addAction(self.next_action) - self.next_action.setEnabled(False) - self.connect(self.treewidget, SIGNAL("set_next_enabled(bool)"), - self.next_action.setEnabled) - - parent_action = create_action(self, text=_("Parent"), - icon=get_std_icon("ArrowUp"), - triggered=self.treewidget.go_to_parent_directory) - self.toolbar.addAction(parent_action) - self.toolbar.addSeparator() - - options_action = create_action(self, text='', tip=_("Options"), - icon=get_icon('tooloptions.png')) - self.toolbar.addAction(options_action) - widget = self.toolbar.widgetForAction(options_action) - widget.setPopupMode(QToolButton.InstantPopup) - menu = QMenu(self) - add_actions(menu, self.treewidget.common_actions) - options_action.setMenu(menu) - - icontext_action.setChecked(show_icontext) - self.toggle_icontext(show_icontext) - - vlayout = QVBoxLayout() - vlayout.addWidget(self.toolbar) - vlayout.addWidget(self.treewidget) - self.setLayout(vlayout) - - def toggle_icontext(self, state): - """Toggle icon text""" - self.sig_option_changed.emit('show_icontext', state) - for action in self.toolbar.actions(): - if not action.isSeparator(): - widget = self.toolbar.widgetForAction(action) - if state: - widget.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) - else: - widget.setToolButtonStyle(Qt.ToolButtonIconOnly) - - -class FileExplorerTest(QWidget): - def __init__(self): - QWidget.__init__(self) - vlayout = QVBoxLayout() - self.setLayout(vlayout) - self.explorer = ExplorerWidget(self, show_cd_only=None) - vlayout.addWidget(self.explorer) - - hlayout1 = QHBoxLayout() - vlayout.addLayout(hlayout1) - label = QLabel("Open file:") - label.setAlignment(Qt.AlignRight) - hlayout1.addWidget(label) - self.label1 = QLabel() - hlayout1.addWidget(self.label1) - self.explorer.sig_open_file.connect(self.label1.setText) - - hlayout2 = QHBoxLayout() - vlayout.addLayout(hlayout2) - label = QLabel("Open dir:") - label.setAlignment(Qt.AlignRight) - hlayout2.addWidget(label) - self.label2 = QLabel() - hlayout2.addWidget(self.label2) - self.connect(self.explorer, SIGNAL("open_dir(QString)"), - self.label2.setText) - - hlayout3 = QHBoxLayout() - vlayout.addLayout(hlayout3) - label = QLabel("Option changed:") - label.setAlignment(Qt.AlignRight) - hlayout3.addWidget(label) - self.label3 = QLabel() - hlayout3.addWidget(self.label3) - self.explorer.sig_option_changed.connect( - lambda x, y: self.label3.setText('option_changed: %r, %r' % (x, y))) - - self.connect(self.explorer, SIGNAL("open_parent_dir()"), - lambda: self.explorer.listwidget.refresh('..')) - -class ProjectExplorerTest(QWidget): - def __init__(self, parent=None): - QWidget.__init__(self, parent) - vlayout = QVBoxLayout() - self.setLayout(vlayout) - self.treewidget = FilteredDirView(self) - self.treewidget.setup_view() - self.treewidget.set_root_path(r'D:\Python') - self.treewidget.set_folder_names(['spyder', 'spyder-2.0']) - vlayout.addWidget(self.treewidget) - - -if __name__ == "__main__": - from spyderlib.utils.qthelpers import qapplication - app = qapplication() - test = FileExplorerTest() -# test = ProjectExplorerTest() - test.resize(640, 480) - test.show() - sys.exit(app.exec_()) - +# -*- coding: utf-8 -*- +# +# Copyright © Spyder Project Contributors +# Licensed under the terms of the MIT License +# (see spyder/__init__.py for details) + +"""Files and Directories Explorer""" + +# pylint: disable=C0103 +# pylint: disable=R0903 +# pylint: disable=R0911 +# pylint: disable=R0201 + +# Standard library imports +from __future__ import with_statement +import os +import os.path as osp +import re +import shutil + +# Third party imports +from qtpy import API, is_pyqt46 +from qtpy.compat import getsavefilename, getexistingdirectory +from qtpy.QtCore import (QDir, QFileInfo, QMimeData, QSize, + QSortFilterProxyModel, Qt, QTimer, QUrl, + Signal, Slot) +from qtpy.QtGui import QDrag +from qtpy.QtWidgets import (QFileSystemModel, QHBoxLayout, QFileIconProvider, + QInputDialog, QLabel, QLineEdit, QMenu, + QMessageBox, QToolButton, QTreeView, QVBoxLayout, + QWidget) +# Local imports +from spyder.config.base import _ +from spyder.py3compat import (getcwd, str_lower, to_binary_string, + to_text_string, PY2) +from spyder.utils import icon_manager as ima +from spyder.utils import encoding, misc, programs, vcs +from spyder.utils.qthelpers import add_actions, create_action, file_uri + +try: + from nbconvert import PythonExporter as nbexporter +except: + nbexporter = None # analysis:ignore + + +def fixpath(path): + """Normalize path fixing case, making absolute and removing symlinks""" + norm = osp.normcase if os.name == 'nt' else osp.normpath + return norm(osp.abspath(osp.realpath(path))) + + +def create_script(fname): + """Create a new Python script""" + text = os.linesep.join(["# -*- coding: utf-8 -*-", "", ""]) + encoding.write(to_text_string(text), fname, 'utf-8') + + +def listdir(path, include='.', exclude=r'\.pyc$|^\.', show_all=False, + folders_only=False): + """List files and directories""" + namelist = [] + dirlist = [to_text_string(osp.pardir)] + for item in os.listdir(to_text_string(path)): + if re.search(exclude, item) and not show_all: + continue + if osp.isdir(osp.join(path, item)): + dirlist.append(item) + elif folders_only: + continue + elif re.search(include, item) or show_all: + namelist.append(item) + return sorted(dirlist, key=str_lower) + \ + sorted(namelist, key=str_lower) + + +def has_subdirectories(path, include, exclude, show_all): + """Return True if path has subdirectories""" + try: + # > 1 because of '..' + return len( listdir(path, include, exclude, + show_all, folders_only=True) ) > 1 + except (IOError, OSError): + return False + + +class IconProvider(QFileIconProvider): + """Project tree widget icon provider""" + def __init__(self, treeview): + super(IconProvider, self).__init__() + self.treeview = treeview + + @Slot(int) + @Slot(QFileInfo) + def icon(self, icontype_or_qfileinfo): + """Reimplement Qt method""" + if isinstance(icontype_or_qfileinfo, QFileIconProvider.IconType): + return super(IconProvider, self).icon(icontype_or_qfileinfo) + else: + qfileinfo = icontype_or_qfileinfo + fname = osp.normpath(to_text_string(qfileinfo.absoluteFilePath())) + if osp.isdir(fname): + return ima.icon('DirOpenIcon') + else: + return ima.icon('FileIcon') + + +class DirView(QTreeView): + """Base file/directory tree view""" + def __init__(self, parent=None): + super(DirView, self).__init__(parent) + self.name_filters = ['*.py'] + self.parent_widget = parent + self.show_all = None + self.menu = None + self.common_actions = None + self.__expanded_state = None + self._to_be_loaded = None + self.fsmodel = None + self.setup_fs_model() + self._scrollbar_positions = None + + #---- Model + def setup_fs_model(self): + """Setup filesystem model""" + filters = QDir.AllDirs | QDir.Files | QDir.Drives | QDir.NoDotAndDotDot + self.fsmodel = QFileSystemModel(self) + self.fsmodel.setFilter(filters) + self.fsmodel.setNameFilterDisables(False) + + def install_model(self): + """Install filesystem model""" + self.setModel(self.fsmodel) + + def setup_view(self): + """Setup view""" + self.install_model() + if not is_pyqt46: + self.fsmodel.directoryLoaded.connect( + lambda: self.resizeColumnToContents(0)) + self.setAnimated(False) + self.setSortingEnabled(True) + self.sortByColumn(0, Qt.AscendingOrder) + self.fsmodel.modelReset.connect(self.reset_icon_provider) + self.reset_icon_provider() + # Disable the view of .spyproject. + self.filter_directories() + + def set_name_filters(self, name_filters): + """Set name filters""" + self.name_filters = name_filters + self.fsmodel.setNameFilters(name_filters) + + def set_show_all(self, state): + """Toggle 'show all files' state""" + if state: + self.fsmodel.setNameFilters([]) + else: + self.fsmodel.setNameFilters(self.name_filters) + + def get_filename(self, index): + """Return filename associated with *index*""" + if index: + return osp.normpath(to_text_string(self.fsmodel.filePath(index))) + + def get_index(self, filename): + """Return index associated with filename""" + return self.fsmodel.index(filename) + + def get_selected_filenames(self): + """Return selected filenames""" + if self.selectionMode() == self.ExtendedSelection: + return [self.get_filename(idx) for idx in self.selectedIndexes()] + else: + return [self.get_filename(self.currentIndex())] + + def get_dirname(self, index): + """Return dirname associated with *index*""" + fname = self.get_filename(index) + if fname: + if osp.isdir(fname): + return fname + else: + return osp.dirname(fname) + + #---- Tree view widget + def setup(self, name_filters=['*.py', '*.pyw'], show_all=False): + """Setup tree widget""" + self.setup_view() + + self.set_name_filters(name_filters) + self.show_all = show_all + + # Setup context menu + self.menu = QMenu(self) + self.common_actions = self.setup_common_actions() + + def reset_icon_provider(self): + """Reset file system model icon provider + The purpose of this is to refresh files/directories icons""" + self.fsmodel.setIconProvider(IconProvider(self)) + + #---- Context menu + def setup_common_actions(self): + """Setup context menu common actions""" + # Filters + filters_action = create_action(self, _("Edit filename filters..."), + None, ima.icon('filter'), + triggered=self.edit_filter) + # Show all files + all_action = create_action(self, _("Show all files"), + toggled=self.toggle_all) + all_action.setChecked(self.show_all) + self.toggle_all(self.show_all) + + return [filters_action, all_action] + + @Slot() + def edit_filter(self): + """Edit name filters""" + filters, valid = QInputDialog.getText(self, _('Edit filename filters'), + _('Name filters:'), + QLineEdit.Normal, + ", ".join(self.name_filters)) + if valid: + filters = [f.strip() for f in to_text_string(filters).split(',')] + self.parent_widget.sig_option_changed.emit('name_filters', filters) + self.set_name_filters(filters) + + @Slot(bool) + def toggle_all(self, checked): + """Toggle all files mode""" + self.parent_widget.sig_option_changed.emit('show_all', checked) + self.show_all = checked + self.set_show_all(checked) + + def create_file_new_actions(self, fnames): + """Return actions for submenu 'New...'""" + if not fnames: + return [] + new_file_act = create_action(self, _("File..."), + icon=ima.icon('filenew'), + triggered=lambda: + self.new_file(fnames[-1])) + new_module_act = create_action(self, _("Module..."), + icon=ima.icon('spyder'), + triggered=lambda: + self.new_module(fnames[-1])) + new_folder_act = create_action(self, _("Folder..."), + icon=ima.icon('folder_new'), + triggered=lambda: + self.new_folder(fnames[-1])) + new_package_act = create_action(self, _("Package..."), + icon=ima.icon('package_new'), + triggered=lambda: + self.new_package(fnames[-1])) + return [new_file_act, new_folder_act, None, + new_module_act, new_package_act] + + def create_file_import_actions(self, fnames): + """Return actions for submenu 'Import...'""" + return [] + + def create_file_manage_actions(self, fnames): + """Return file management actions""" + only_files = all([osp.isfile(_fn) for _fn in fnames]) + only_modules = all([osp.splitext(_fn)[1] in ('.py', '.pyw', '.ipy') + for _fn in fnames]) + only_notebooks = all([osp.splitext(_fn)[1] == '.ipynb' + for _fn in fnames]) + only_valid = all([encoding.is_text_file(_fn) for _fn in fnames]) + run_action = create_action(self, _("Run"), icon=ima.icon('run'), + triggered=self.run) + edit_action = create_action(self, _("Edit"), icon=ima.icon('edit'), + triggered=self.clicked) + move_action = create_action(self, _("Move..."), + icon="move.png", + triggered=self.move) + delete_action = create_action(self, _("Delete..."), + icon=ima.icon('editdelete'), + triggered=self.delete) + rename_action = create_action(self, _("Rename..."), + icon=ima.icon('rename'), + triggered=self.rename) + open_action = create_action(self, _("Open"), triggered=self.open) + ipynb_convert_action = create_action(self, _("Convert to Python script"), + icon=ima.icon('python'), + triggered=self.convert_notebooks) + + actions = [] + if only_modules: + actions.append(run_action) + if only_valid and only_files: + actions.append(edit_action) + else: + actions.append(open_action) + actions += [delete_action, rename_action] + basedir = fixpath(osp.dirname(fnames[0])) + if all([fixpath(osp.dirname(_fn)) == basedir for _fn in fnames]): + actions.append(move_action) + actions += [None] + if only_notebooks and nbexporter is not None: + actions.append(ipynb_convert_action) + + # VCS support is quite limited for now, so we are enabling the VCS + # related actions only when a single file/folder is selected: + dirname = fnames[0] if osp.isdir(fnames[0]) else osp.dirname(fnames[0]) + if len(fnames) == 1 and vcs.is_vcs_repository(dirname): + # QAction.triggered works differently for PySide and PyQt + if not API == 'pyside': + commit_slot = lambda _checked, fnames=[dirname]:\ + self.vcs_command(fnames, 'commit') + browse_slot = lambda _checked, fnames=[dirname]:\ + self.vcs_command(fnames, 'browse') + else: + commit_slot = lambda fnames=[dirname]:\ + self.vcs_command(fnames, 'commit') + browse_slot = lambda fnames=[dirname]:\ + self.vcs_command(fnames, 'browse') + vcs_ci = create_action(self, _("Commit"), + icon=ima.icon('vcs_commit'), + triggered=commit_slot) + vcs_log = create_action(self, _("Browse repository"), + icon=ima.icon('vcs_browse'), + triggered=browse_slot) + actions += [None, vcs_ci, vcs_log] + + return actions + + def create_folder_manage_actions(self, fnames): + """Return folder management actions""" + actions = [] + if os.name == 'nt': + _title = _("Open command prompt here") + else: + _title = _("Open terminal here") + action = create_action(self, _title, icon=ima.icon('cmdprompt'), + triggered=lambda: + self.open_terminal(fnames)) + actions.append(action) + _title = _("Open Python console here") + action = create_action(self, _title, icon=ima.icon('python'), + triggered=lambda: + self.open_interpreter(fnames)) + actions.append(action) + return actions + + def create_context_menu_actions(self): + """Create context menu actions""" + actions = [] + fnames = self.get_selected_filenames() + new_actions = self.create_file_new_actions(fnames) + if len(new_actions) > 1: + # Creating a submenu only if there is more than one entry + new_act_menu = QMenu(_('New'), self) + add_actions(new_act_menu, new_actions) + actions.append(new_act_menu) + else: + actions += new_actions + import_actions = self.create_file_import_actions(fnames) + if len(import_actions) > 1: + # Creating a submenu only if there is more than one entry + import_act_menu = QMenu(_('Import'), self) + add_actions(import_act_menu, import_actions) + actions.append(import_act_menu) + else: + actions += import_actions + if actions: + actions.append(None) + if fnames: + actions += self.create_file_manage_actions(fnames) + if actions: + actions.append(None) + if fnames and all([osp.isdir(_fn) for _fn in fnames]): + actions += self.create_folder_manage_actions(fnames) + if actions: + actions.append(None) + actions += self.common_actions + return actions + + def update_menu(self): + """Update context menu""" + self.menu.clear() + add_actions(self.menu, self.create_context_menu_actions()) + + #---- Events + def viewportEvent(self, event): + """Reimplement Qt method""" + + # Prevent Qt from crashing or showing warnings like: + # "QSortFilterProxyModel: index from wrong model passed to + # mapFromSource", probably due to the fact that the file system model + # is being built. See Issue 1250. + # + # This workaround was inspired by the following KDE bug: + # https://bugs.kde.org/show_bug.cgi?id=172198 + # + # Apparently, this is a bug from Qt itself. + self.executeDelayedItemsLayout() + + return QTreeView.viewportEvent(self, event) + + def contextMenuEvent(self, event): + """Override Qt method""" + self.update_menu() + self.menu.popup(event.globalPos()) + + def keyPressEvent(self, event): + """Reimplement Qt method""" + if event.key() in (Qt.Key_Enter, Qt.Key_Return): + self.clicked() + elif event.key() == Qt.Key_F2: + self.rename() + elif event.key() == Qt.Key_Delete: + self.delete() + elif event.key() == Qt.Key_Backspace: + self.go_to_parent_directory() + else: + QTreeView.keyPressEvent(self, event) + + def mouseDoubleClickEvent(self, event): + """Reimplement Qt method""" + QTreeView.mouseDoubleClickEvent(self, event) + self.clicked() + + @Slot() + def clicked(self): + """Selected item was double-clicked or enter/return was pressed""" + fnames = self.get_selected_filenames() + for fname in fnames: + if osp.isdir(fname): + self.directory_clicked(fname) + else: + self.open([fname]) + + def directory_clicked(self, dirname): + """Directory was just clicked""" + pass + + #---- Drag + def dragEnterEvent(self, event): + """Drag and Drop - Enter event""" + event.setAccepted(event.mimeData().hasFormat("text/plain")) + + def dragMoveEvent(self, event): + """Drag and Drop - Move event""" + if (event.mimeData().hasFormat("text/plain")): + event.setDropAction(Qt.MoveAction) + event.accept() + else: + event.ignore() + + def startDrag(self, dropActions): + """Reimplement Qt Method - handle drag event""" + data = QMimeData() + data.setUrls([QUrl(fname) for fname in self.get_selected_filenames()]) + drag = QDrag(self) + drag.setMimeData(data) + drag.exec_() + + #---- File/Directory actions + @Slot() + def open(self, fnames=None): + """Open files with the appropriate application""" + if fnames is None: + fnames = self.get_selected_filenames() + for fname in fnames: + if osp.isfile(fname) and encoding.is_text_file(fname): + self.parent_widget.sig_open_file.emit(fname) + else: + self.open_outside_spyder([fname]) + + def open_outside_spyder(self, fnames): + """Open file outside Spyder with the appropriate application + If this does not work, opening unknown file in Spyder, as text file""" + for path in sorted(fnames): + path = file_uri(path) + ok = programs.start_file(path) + if not ok: + self.parent_widget.edit.emit(path) + + def open_terminal(self, fnames): + """Open terminal""" + for path in sorted(fnames): + self.parent_widget.open_terminal.emit(path) + + def open_interpreter(self, fnames): + """Open interpreter""" + for path in sorted(fnames): + self.parent_widget.open_interpreter.emit(path) + + @Slot() + def run(self, fnames=None): + """Run Python scripts""" + if fnames is None: + fnames = self.get_selected_filenames() + for fname in fnames: + self.parent_widget.run.emit(fname) + + def remove_tree(self, dirname): + """Remove whole directory tree + Reimplemented in project explorer widget""" + shutil.rmtree(dirname, onerror=misc.onerror) + + def delete_file(self, fname, multiple, yes_to_all): + """Delete file""" + if multiple: + buttons = QMessageBox.Yes|QMessageBox.YesAll| \ + QMessageBox.No|QMessageBox.Cancel + else: + buttons = QMessageBox.Yes|QMessageBox.No + if yes_to_all is None: + answer = QMessageBox.warning(self, _("Delete"), + _("Do you really want " + "to delete %s?" + ) % osp.basename(fname), buttons) + if answer == QMessageBox.No: + return yes_to_all + elif answer == QMessageBox.Cancel: + return False + elif answer == QMessageBox.YesAll: + yes_to_all = True + try: + if osp.isfile(fname): + misc.remove_file(fname) + self.parent_widget.removed.emit(fname) + else: + self.remove_tree(fname) + self.parent_widget.removed_tree.emit(fname) + return yes_to_all + except EnvironmentError as error: + action_str = _('delete') + QMessageBox.critical(self, _("Project Explorer"), + _("Unable to %s %s" + "

    Error message:
    %s" + ) % (action_str, fname, to_text_string(error))) + return False + + @Slot() + def delete(self, fnames=None): + """Delete files""" + if fnames is None: + fnames = self.get_selected_filenames() + multiple = len(fnames) > 1 + yes_to_all = None + for fname in fnames: + spyproject_path = osp.join(fname,'.spyproject') + if osp.isdir(fname) and osp.exists(spyproject_path): + QMessageBox.information(self, _('File Explorer'), + _("The current directory contains a " + "project.

    " + "If you want to delete" + " the project, please go to " + "Projects » Delete " + "Project")) + else: + yes_to_all = self.delete_file(fname, multiple, yes_to_all) + if yes_to_all is not None and not yes_to_all: + # Canceled + break + + def convert_notebook(self, fname): + """Convert an IPython notebook to a Python script in editor""" + try: + script = nbexporter().from_filename(fname)[0] + except Exception as e: + QMessageBox.critical(self, _('Conversion error'), + _("It was not possible to convert this " + "notebook. The error is:\n\n") + \ + to_text_string(e)) + return + self.parent_widget.sig_new_file.emit(script) + + @Slot() + def convert_notebooks(self): + """Convert IPython notebooks to Python scripts in editor""" + fnames = self.get_selected_filenames() + if not isinstance(fnames, (tuple, list)): + fnames = [fnames] + for fname in fnames: + self.convert_notebook(fname) + + def rename_file(self, fname): + """Rename file""" + path, valid = QInputDialog.getText(self, _('Rename'), + _('New name:'), QLineEdit.Normal, + osp.basename(fname)) + if valid: + path = osp.join(osp.dirname(fname), to_text_string(path)) + if path == fname: + return + if osp.exists(path): + if QMessageBox.warning(self, _("Rename"), + _("Do you really want to rename %s and " + "overwrite the existing file %s?" + ) % (osp.basename(fname), osp.basename(path)), + QMessageBox.Yes|QMessageBox.No) == QMessageBox.No: + return + try: + misc.rename_file(fname, path) + self.parent_widget.renamed.emit(fname, path) + return path + except EnvironmentError as error: + QMessageBox.critical(self, _("Rename"), + _("Unable to rename file %s" + "

    Error message:
    %s" + ) % (osp.basename(fname), to_text_string(error))) + + @Slot() + def rename(self, fnames=None): + """Rename files""" + if fnames is None: + fnames = self.get_selected_filenames() + if not isinstance(fnames, (tuple, list)): + fnames = [fnames] + for fname in fnames: + self.rename_file(fname) + + @Slot() + def move(self, fnames=None): + """Move files/directories""" + if fnames is None: + fnames = self.get_selected_filenames() + orig = fixpath(osp.dirname(fnames[0])) + while True: + self.parent_widget.redirect_stdio.emit(False) + folder = getexistingdirectory(self, _("Select directory"), orig) + self.parent_widget.redirect_stdio.emit(True) + if folder: + folder = fixpath(folder) + if folder != orig: + break + else: + return + for fname in fnames: + basename = osp.basename(fname) + try: + misc.move_file(fname, osp.join(folder, basename)) + except EnvironmentError as error: + QMessageBox.critical(self, _("Error"), + _("Unable to move %s" + "

    Error message:
    %s" + ) % (basename, to_text_string(error))) + + def create_new_folder(self, current_path, title, subtitle, is_package): + """Create new folder""" + if current_path is None: + current_path = '' + if osp.isfile(current_path): + current_path = osp.dirname(current_path) + name, valid = QInputDialog.getText(self, title, subtitle, + QLineEdit.Normal, "") + if valid: + dirname = osp.join(current_path, to_text_string(name)) + try: + os.mkdir(dirname) + except EnvironmentError as error: + QMessageBox.critical(self, title, + _("Unable " + "to create folder %s" + "

    Error message:
    %s" + ) % (dirname, to_text_string(error))) + finally: + if is_package: + fname = osp.join(dirname, '__init__.py') + try: + with open(fname, 'wb') as f: + f.write(to_binary_string('#')) + return dirname + except EnvironmentError as error: + QMessageBox.critical(self, title, + _("Unable " + "to create file %s" + "

    Error message:
    %s" + ) % (fname, + to_text_string(error))) + + def new_folder(self, basedir): + """New folder""" + title = _('New folder') + subtitle = _('Folder name:') + self.create_new_folder(basedir, title, subtitle, is_package=False) + + def new_package(self, basedir): + """New package""" + title = _('New package') + subtitle = _('Package name:') + self.create_new_folder(basedir, title, subtitle, is_package=True) + + def create_new_file(self, current_path, title, filters, create_func): + """Create new file + Returns True if successful""" + if current_path is None: + current_path = '' + if osp.isfile(current_path): + current_path = osp.dirname(current_path) + self.parent_widget.redirect_stdio.emit(False) + fname, _selfilter = getsavefilename(self, title, current_path, filters) + self.parent_widget.redirect_stdio.emit(True) + if fname: + try: + create_func(fname) + return fname + except EnvironmentError as error: + QMessageBox.critical(self, _("New file"), + _("Unable to create file %s" + "

    Error message:
    %s" + ) % (fname, to_text_string(error))) + + def new_file(self, basedir): + """New file""" + title = _("New file") + filters = _("All files")+" (*)" + def create_func(fname): + """File creation callback""" + if osp.splitext(fname)[1] in ('.py', '.pyw', '.ipy'): + create_script(fname) + else: + with open(fname, 'wb') as f: + f.write(to_binary_string('')) + fname = self.create_new_file(basedir, title, filters, create_func) + if fname is not None: + self.open([fname]) + + def new_module(self, basedir): + """New module""" + title = _("New module") + filters = _("Python scripts")+" (*.py *.pyw *.ipy)" + create_func = lambda fname: self.parent_widget.create_module.emit(fname) + self.create_new_file(basedir, title, filters, create_func) + + def go_to_parent_directory(self): + pass + + #----- VCS actions + def vcs_command(self, fnames, action): + """VCS action (commit, browse)""" + try: + for path in sorted(fnames): + vcs.run_vcs_tool(path, action) + except vcs.ActionToolNotFound as error: + msg = _("For %s support, please install one of the
    " + "following tools:

    %s")\ + % (error.vcsname, ', '.join(error.tools)) + QMessageBox.critical(self, _("Error"), + _("""Unable to find external program.

    %s""") + % to_text_string(msg)) + + #----- Settings + def get_scrollbar_position(self): + """Return scrollbar positions""" + return (self.horizontalScrollBar().value(), + self.verticalScrollBar().value()) + + def set_scrollbar_position(self, position): + """Set scrollbar positions""" + # Scrollbars will be restored after the expanded state + self._scrollbar_positions = position + if self._to_be_loaded is not None and len(self._to_be_loaded) == 0: + self.restore_scrollbar_positions() + + def restore_scrollbar_positions(self): + """Restore scrollbar positions once tree is loaded""" + hor, ver = self._scrollbar_positions + self.horizontalScrollBar().setValue(hor) + self.verticalScrollBar().setValue(ver) + + def get_expanded_state(self): + """Return expanded state""" + self.save_expanded_state() + return self.__expanded_state + + def set_expanded_state(self, state): + """Set expanded state""" + self.__expanded_state = state + self.restore_expanded_state() + + def save_expanded_state(self): + """Save all items expanded state""" + model = self.model() + # If model is not installed, 'model' will be None: this happens when + # using the Project Explorer without having selected a workspace yet + if model is not None: + self.__expanded_state = [] + for idx in model.persistentIndexList(): + if self.isExpanded(idx): + self.__expanded_state.append(self.get_filename(idx)) + + def restore_directory_state(self, fname): + """Restore directory expanded state""" + root = osp.normpath(to_text_string(fname)) + if not osp.exists(root): + # Directory has been (re)moved outside Spyder + return + for basename in os.listdir(root): + path = osp.normpath(osp.join(root, basename)) + if osp.isdir(path) and path in self.__expanded_state: + self.__expanded_state.pop(self.__expanded_state.index(path)) + if self._to_be_loaded is None: + self._to_be_loaded = [] + self._to_be_loaded.append(path) + self.setExpanded(self.get_index(path), True) + if not self.__expanded_state and not is_pyqt46: + self.fsmodel.directoryLoaded.disconnect(self.restore_directory_state) + + def follow_directories_loaded(self, fname): + """Follow directories loaded during startup""" + if self._to_be_loaded is None: + return + path = osp.normpath(to_text_string(fname)) + if path in self._to_be_loaded: + self._to_be_loaded.remove(path) + if self._to_be_loaded is not None and len(self._to_be_loaded) == 0 \ + and not is_pyqt46: + self.fsmodel.directoryLoaded.disconnect( + self.follow_directories_loaded) + if self._scrollbar_positions is not None: + # The tree view need some time to render branches: + QTimer.singleShot(50, self.restore_scrollbar_positions) + + def restore_expanded_state(self): + """Restore all items expanded state""" + if self.__expanded_state is not None: + # In the old project explorer, the expanded state was a dictionnary: + if isinstance(self.__expanded_state, list) and not is_pyqt46: + self.fsmodel.directoryLoaded.connect( + self.restore_directory_state) + self.fsmodel.directoryLoaded.connect( + self.follow_directories_loaded) + + def filter_directories(self): + """Filter the directories to show""" + index = self.get_index('.spyproject') + if index is not None: + self.setRowHidden(index.row(), index.parent(), True) + +class ProxyModel(QSortFilterProxyModel): + """Proxy model: filters tree view""" + def __init__(self, parent): + super(ProxyModel, self).__init__(parent) + self.root_path = None + self.path_list = [] + self.setDynamicSortFilter(True) + + def setup_filter(self, root_path, path_list): + """Setup proxy model filter parameters""" + self.root_path = osp.normpath(to_text_string(root_path)) + self.path_list = [osp.normpath(to_text_string(p)) for p in path_list] + self.invalidateFilter() + + def sort(self, column, order=Qt.AscendingOrder): + """Reimplement Qt method""" + self.sourceModel().sort(column, order) + + def filterAcceptsRow(self, row, parent_index): + """Reimplement Qt method""" + if self.root_path is None: + return True + index = self.sourceModel().index(row, 0, parent_index) + path = osp.normpath(to_text_string(self.sourceModel().filePath(index))) + if self.root_path.startswith(path): + # This is necessary because parent folders need to be scanned + return True + else: + for p in self.path_list: + if path == p or path.startswith(p+os.sep): + return True + else: + return False + + def data(self, index, role): + """Show tooltip with full path only for the root directory""" + if role == Qt.ToolTipRole: + root_dir = self.path_list[0].split(osp.sep)[-1] + if index.data() == root_dir: + return osp.join(self.root_path, root_dir) + return QSortFilterProxyModel.data(self, index, role) + +class FilteredDirView(DirView): + """Filtered file/directory tree view""" + def __init__(self, parent=None): + super(FilteredDirView, self).__init__(parent) + self.proxymodel = None + self.setup_proxy_model() + self.root_path = None + + #---- Model + def setup_proxy_model(self): + """Setup proxy model""" + self.proxymodel = ProxyModel(self) + self.proxymodel.setSourceModel(self.fsmodel) + + def install_model(self): + """Install proxy model""" + if self.root_path is not None: + self.setModel(self.proxymodel) + + def set_root_path(self, root_path): + """Set root path""" + self.root_path = root_path + self.install_model() + index = self.fsmodel.setRootPath(root_path) + self.setRootIndex(self.proxymodel.mapFromSource(index)) + + def get_index(self, filename): + """Return index associated with filename""" + index = self.fsmodel.index(filename) + if index.isValid() and index.model() is self.fsmodel: + return self.proxymodel.mapFromSource(index) + + def set_folder_names(self, folder_names): + """Set folder names""" + assert self.root_path is not None + path_list = [osp.join(self.root_path, dirname) + for dirname in folder_names] + self.proxymodel.setup_filter(self.root_path, path_list) + + def get_filename(self, index): + """Return filename from index""" + if index: + path = self.fsmodel.filePath(self.proxymodel.mapToSource(index)) + return osp.normpath(to_text_string(path)) + + def setup_project_view(self): + """Setup view for projects""" + for i in [1, 2, 3]: + self.hideColumn(i) + self.setHeaderHidden(True) + # Disable the view of .spyproject. + self.filter_directories() + +class ExplorerTreeWidget(DirView): + """File/directory explorer tree widget + show_cd_only: Show current directory only + (True/False: enable/disable the option + None: enable the option and do not allow the user to disable it)""" + set_previous_enabled = Signal(bool) + set_next_enabled = Signal(bool) + + def __init__(self, parent=None, show_cd_only=None): + DirView.__init__(self, parent) + + self.history = [] + self.histindex = None + + self.show_cd_only = show_cd_only + self.__original_root_index = None + self.__last_folder = None + + self.menu = None + self.common_actions = None + + # Enable drag events + self.setDragEnabled(True) + + #---- Context menu + def setup_common_actions(self): + """Setup context menu common actions""" + actions = super(ExplorerTreeWidget, self).setup_common_actions() + if self.show_cd_only is None: + # Enabling the 'show current directory only' option but do not + # allow the user to disable it + self.show_cd_only = True + else: + # Show current directory only + cd_only_action = create_action(self, + _("Show current directory only"), + toggled=self.toggle_show_cd_only) + cd_only_action.setChecked(self.show_cd_only) + self.toggle_show_cd_only(self.show_cd_only) + actions.append(cd_only_action) + return actions + + @Slot(bool) + def toggle_show_cd_only(self, checked): + """Toggle show current directory only mode""" + self.parent_widget.sig_option_changed.emit('show_cd_only', checked) + self.show_cd_only = checked + if checked: + if self.__last_folder is not None: + self.set_current_folder(self.__last_folder) + elif self.__original_root_index is not None: + self.setRootIndex(self.__original_root_index) + + #---- Refreshing widget + def set_current_folder(self, folder): + """Set current folder and return associated model index""" + index = self.fsmodel.setRootPath(folder) + self.__last_folder = folder + if self.show_cd_only: + if self.__original_root_index is None: + self.__original_root_index = self.rootIndex() + self.setRootIndex(index) + return index + + def refresh(self, new_path=None, force_current=False): + """Refresh widget + force=False: won't refresh widget if path has not changed""" + if new_path is None: + new_path = getcwd() + if force_current: + index = self.set_current_folder(new_path) + self.expand(index) + self.setCurrentIndex(index) + self.set_previous_enabled.emit( + self.histindex is not None and self.histindex > 0) + self.set_next_enabled.emit(self.histindex is not None and \ + self.histindex < len(self.history)-1) + # Disable the view of .spyproject. + self.filter_directories() + + #---- Events + def directory_clicked(self, dirname): + """Directory was just clicked""" + self.chdir(directory=dirname) + + #---- Files/Directories Actions + @Slot() + def go_to_parent_directory(self): + """Go to parent directory""" + self.chdir( osp.abspath(osp.join(getcwd(), os.pardir)) ) + + @Slot() + def go_to_previous_directory(self): + """Back to previous directory""" + self.histindex -= 1 + self.chdir(browsing_history=True) + + @Slot() + def go_to_next_directory(self): + """Return to next directory""" + self.histindex += 1 + self.chdir(browsing_history=True) + + def update_history(self, directory): + """Update browse history""" + directory = osp.abspath(to_text_string(directory)) + if directory in self.history: + self.histindex = self.history.index(directory) + + def chdir(self, directory=None, browsing_history=False): + """Set directory as working directory""" + if directory is not None: + directory = osp.abspath(to_text_string(directory)) + if browsing_history: + directory = self.history[self.histindex] + elif directory in self.history: + self.histindex = self.history.index(directory) + else: + if self.histindex is None: + self.history = [] + else: + self.history = self.history[:self.histindex+1] + if len(self.history) == 0 or \ + (self.history and self.history[-1] != directory): + self.history.append(directory) + self.histindex = len(self.history)-1 + directory = to_text_string(directory) + if PY2: + PermissionError = OSError + try: + os.chdir(directory) + self.parent_widget.open_dir.emit(directory) + self.refresh(new_path=directory, force_current=True) + except PermissionError: + QMessageBox.critical(self.parent_widget, "Error", + _("You don't have the right permissions to " + "open this directory")) + + +class ExplorerWidget(QWidget): + """Explorer widget""" + sig_option_changed = Signal(str, object) + sig_open_file = Signal(str) + sig_new_file = Signal(str) + redirect_stdio = Signal(bool) + open_dir = Signal(str) + + def __init__(self, parent=None, name_filters=['*.py', '*.pyw'], + show_all=False, show_cd_only=None, show_icontext=True): + QWidget.__init__(self, parent) + + # Widgets + self.treewidget = ExplorerTreeWidget(self, show_cd_only=show_cd_only) + button_previous = QToolButton(self) + button_next = QToolButton(self) + button_parent = QToolButton(self) + self.button_menu = QToolButton(self) + menu = QMenu(self) + + self.action_widgets = [button_previous, button_next, button_parent, + self.button_menu] + + # Actions + icontext_action = create_action(self, _("Show icons and text"), + toggled=self.toggle_icontext) + previous_action = create_action(self, text=_("Previous"), + icon=ima.icon('ArrowBack'), + triggered=self.treewidget.go_to_previous_directory) + next_action = create_action(self, text=_("Next"), + icon=ima.icon('ArrowForward'), + triggered=self.treewidget.go_to_next_directory) + parent_action = create_action(self, text=_("Parent"), + icon=ima.icon('ArrowUp'), + triggered=self.treewidget.go_to_parent_directory) + options_action = create_action(self, text='', tip=_('Options')) + + # Setup widgets + self.treewidget.setup(name_filters=name_filters, show_all=show_all) + self.treewidget.chdir(getcwd()) + self.treewidget.common_actions += [None, icontext_action] + + button_previous.setDefaultAction(previous_action) + previous_action.setEnabled(False) + + button_next.setDefaultAction(next_action) + next_action.setEnabled(False) + + button_parent.setDefaultAction(parent_action) + + self.button_menu.setIcon(ima.icon('tooloptions')) + self.button_menu.setPopupMode(QToolButton.InstantPopup) + self.button_menu.setMenu(menu) + add_actions(menu, self.treewidget.common_actions) + options_action.setMenu(menu) + + self.toggle_icontext(show_icontext) + icontext_action.setChecked(show_icontext) + + for widget in self.action_widgets: + widget.setAutoRaise(True) + widget.setIconSize(QSize(16, 16)) + + # Layouts + blayout = QHBoxLayout() + blayout.addWidget(button_previous) + blayout.addWidget(button_next) + blayout.addWidget(button_parent) + blayout.addStretch() + blayout.addWidget(self.button_menu) + + layout = QVBoxLayout() + layout.addLayout(blayout) + layout.addWidget(self.treewidget) + self.setLayout(layout) + + # Signals and slots + self.treewidget.set_previous_enabled.connect( + previous_action.setEnabled) + self.treewidget.set_next_enabled.connect(next_action.setEnabled) + + @Slot(bool) + def toggle_icontext(self, state): + """Toggle icon text""" + self.sig_option_changed.emit('show_icontext', state) + for widget in self.action_widgets: + if widget is not self.button_menu: + if state: + widget.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) + else: + widget.setToolButtonStyle(Qt.ToolButtonIconOnly) + + +#============================================================================== +# Tests +#============================================================================== +class FileExplorerTest(QWidget): + def __init__(self): + QWidget.__init__(self) + vlayout = QVBoxLayout() + self.setLayout(vlayout) + self.explorer = ExplorerWidget(self, show_cd_only=None) + vlayout.addWidget(self.explorer) + + hlayout1 = QHBoxLayout() + vlayout.addLayout(hlayout1) + label = QLabel("Open file:") + label.setAlignment(Qt.AlignRight) + hlayout1.addWidget(label) + self.label1 = QLabel() + hlayout1.addWidget(self.label1) + self.explorer.sig_open_file.connect(self.label1.setText) + + hlayout2 = QHBoxLayout() + vlayout.addLayout(hlayout2) + label = QLabel("Open dir:") + label.setAlignment(Qt.AlignRight) + hlayout2.addWidget(label) + self.label2 = QLabel() + hlayout2.addWidget(self.label2) + self.explorer.open_dir.connect(self.label2.setText) + + hlayout3 = QHBoxLayout() + vlayout.addLayout(hlayout3) + label = QLabel("Option changed:") + label.setAlignment(Qt.AlignRight) + hlayout3.addWidget(label) + self.label3 = QLabel() + hlayout3.addWidget(self.label3) + self.explorer.sig_option_changed.connect( + lambda x, y: self.label3.setText('option_changed: %r, %r' % (x, y))) + self.explorer.open_dir.connect( + lambda: self.explorer.treewidget.refresh('..')) + + +class ProjectExplorerTest(QWidget): + def __init__(self, parent=None): + QWidget.__init__(self, parent) + vlayout = QVBoxLayout() + self.setLayout(vlayout) + self.treewidget = FilteredDirView(self) + self.treewidget.setup_view() + self.treewidget.set_root_path(osp.dirname(osp.abspath(__file__))) + self.treewidget.set_folder_names(['variableexplorer']) + self.treewidget.setup_project_view() + vlayout.addWidget(self.treewidget) + + +def test(file_explorer): + from spyder.utils.qthelpers import qapplication + app = qapplication() + if file_explorer: + test = FileExplorerTest() + else: + test = ProjectExplorerTest() + test.resize(640, 480) + test.show() + app.exec_() + + +if __name__ == "__main__": + test(file_explorer=True) + test(file_explorer=False) diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/widgets/externalshell/baseshell.py spyder-3.0.2+dfsg1/spyder/widgets/externalshell/baseshell.py --- spyder-2.3.8+dfsg1/spyder/widgets/externalshell/baseshell.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/widgets/externalshell/baseshell.py 2016-10-25 02:05:23.000000000 +0200 @@ -1,54 +1,42 @@ # -*- coding: utf-8 -*- # -# Copyright © 2010 Pierre Raybaut +# Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) +# (see spyder/__init__.py for details) # pylint: disable=C0103 # pylint: disable=R0903 # pylint: disable=R0911 # pylint: disable=R0201 -import sys -import os -import os.path as osp +# Standard library imports from time import time, strftime, gmtime +import os.path as osp -from spyderlib.qt.QtGui import (QApplication, QWidget, QVBoxLayout, - QHBoxLayout, QMenu, QLabel, QInputDialog, - QLineEdit, QToolButton) -from spyderlib.qt.QtCore import (QProcess, SIGNAL, QByteArray, QTimer, Qt, - QTextCodec) -LOCALE_CODEC = QTextCodec.codecForLocale() +# Third party imports +from qtpy.QtCore import (QByteArray, QProcess, Qt, QTextCodec, QTimer, + Signal, Slot, QMutex) +from qtpy.QtWidgets import (QHBoxLayout, QInputDialog, QLabel, QLineEdit, + QMenu, QToolButton, QVBoxLayout, QWidget) # Local imports -from spyderlib.utils.qthelpers import (get_icon, create_toolbutton, - create_action, add_actions) -from spyderlib.baseconfig import get_conf_path, _ -from spyderlib.py3compat import is_text_string, to_text_string - - -def add_pathlist_to_PYTHONPATH(env, pathlist, drop_env=False): - # PyQt API 1/2 compatibility-related tests: - assert isinstance(env, list) - assert all([is_text_string(path) for path in env]) - - pypath = "PYTHONPATH" - pathstr = os.pathsep.join(pathlist) - if os.environ.get(pypath) is not None and not drop_env: - for index, var in enumerate(env[:]): - if var.startswith(pypath+'='): - env[index] = var.replace(pypath+'=', - pypath+'='+pathstr+os.pathsep) - env.append('OLD_PYTHONPATH='+os.environ[pypath]) - else: - env.append(pypath+'='+pathstr) - +from spyder.config.base import _, get_conf_path +from spyder.py3compat import to_text_string +from spyder.utils import icon_manager as ima +from spyder.utils.qthelpers import (add_actions, create_action, + create_toolbutton) + + +LOCALE_CODEC = QTextCodec.codecForLocale() + #TODO: code refactoring/cleaning (together with systemshell.py and pythonshell.py) class ExternalShellBase(QWidget): """External Shell widget: execute Python script in a separate process""" SHELL_CLASS = None + redirect_stdio = Signal(bool) + sig_finished = Signal() + def __init__(self, parent=None, fname=None, wdir=None, history_filename=None, show_icontext=True, light_background=True, menu_actions=None, @@ -56,6 +44,9 @@ QWidget.__init__(self, parent) self.menu_actions = menu_actions + self.write_lock = QMutex() + self.buffer_lock = QMutex() + self.buffer = [] self.run_button = None self.kill_button = None @@ -72,14 +63,11 @@ self.shell = self.SHELL_CLASS(parent, get_conf_path(history_filename)) self.shell.set_light_background(light_background) - self.connect(self.shell, SIGNAL("execute(QString)"), - self.send_to_process) - self.connect(self.shell, SIGNAL("keyboard_interrupt()"), - self.keyboard_interrupt) + self.shell.execute.connect(self.send_to_process) + self.shell.sig_keyboard_interrupt.connect(self.keyboard_interrupt) # Redirecting some SIGNALs: - self.connect(self.shell, SIGNAL('redirect_stdio(bool)'), - lambda state: self.emit(SIGNAL('redirect_stdio(bool)'), - state)) + self.shell.redirect_stdio.connect( + lambda state: self.redirect_stdio.emit(state)) self.state_label = None self.time_label = None @@ -114,7 +102,8 @@ if show_buttons_inside: self.update_time_label_visibility() - + + @Slot(bool) def set_elapsed_time_visible(self, state): self.show_elapsed_time = state if self.time_label is not None: @@ -136,20 +125,20 @@ def get_toolbar_buttons(self): if self.run_button is None: self.run_button = create_toolbutton(self, text=_("Run"), - icon=get_icon('run.png'), + icon=ima.icon('run'), tip=_("Run again this program"), triggered=self.start_shell) if self.kill_button is None: self.kill_button = create_toolbutton(self, text=_("Kill"), - icon=get_icon('kill.png'), + icon=ima.icon('kill'), tip=_("Kills the current process, " "causing it to exit immediately")) buttons = [self.run_button] if self.options_button is None: options = self.get_options_menu() if options: - self.options_button = create_toolbutton(self, text=_("Options"), - icon=get_icon('tooloptions.png')) + self.options_button = create_toolbutton(self, text=_('Options'), + icon=ima.icon('tooloptions')) self.options_button.setPopupMode(QToolButton.InstantPopup) menu = QMenu(self) add_actions(menu, options) @@ -203,7 +192,11 @@ self.is_closing = True self.process.kill() self.process.waitForFinished(100) - self.disconnect(self.timer, SIGNAL("timeout()"), self.show_time) + + try: + self.timer.timeout.disconnect(self.show_time) + except (RuntimeError, TypeError): + pass def set_running_state(self, state=True): self.set_buttons_runnning_state(state) @@ -213,31 +206,34 @@ self.state_label.setText(_( "Running...")) self.t0 = time() - self.connect(self.timer, SIGNAL("timeout()"), self.show_time) + self.timer.timeout.connect(self.show_time) self.timer.start(1000) else: if self.state_label is not None: self.state_label.setText(_('Terminated.')) - self.disconnect(self.timer, SIGNAL("timeout()"), self.show_time) + try: + self.timer.timeout.disconnect(self.show_time) + except (RuntimeError, TypeError): + pass def set_buttons_runnning_state(self, state): - self.run_button.setVisible(not state and not self.is_ipykernel) + self.run_button.setVisible(not state) self.kill_button.setVisible(state) - + + @Slot(bool) def start_shell(self, ask_for_arguments=False): """Start shell""" if ask_for_arguments and not self.get_arguments(): self.set_running_state(False) return try: - self.disconnect(self.terminate_button, SIGNAL("clicked()"), - self.process.terminate) - self.disconnect(self.kill_button, SIGNAL("clicked()"), - self.process.terminate) - except: + self.terminate_button.clicked.disconnect(self.process.terminate) + self.kill_button.clicked.disconnect(self.process.terminate) + except (AttributeError, RuntimeError, TypeError): pass self.create_process() + @Slot() def get_arguments(self): arguments, valid = QInputDialog.getText(self, _('Arguments'), _('Command line arguments:'), @@ -252,7 +248,7 @@ def finished(self, exit_code, exit_status): self.shell.flush() - self.emit(SIGNAL('finished()')) + self.sig_finished.emit() if self.is_closing: return self.set_running_state(False) @@ -280,14 +276,36 @@ while self.process.bytesAvailable(): qba += self.process.readAllStandardError() return self.transcode(qba) - + def write_output(self): + # if we are already writing something else, + # store the present message in a buffer + if not self.write_lock.tryLock(): + self.buffer_lock.lock() + self.buffer.append(self.get_stdout()) + self.buffer_lock.unlock() + + if not self.write_lock.tryLock(): + return + self.shell.write(self.get_stdout(), flush=True) - QApplication.processEvents() - + + while True: + self.buffer_lock.lock() + messages = self.buffer + self.buffer = [] + + if not messages: + self.write_lock.unlock() + self.buffer_lock.unlock() + return + + self.buffer_lock.unlock() + self.shell.write("\n".join(messages), flush=True) + def send_to_process(self, qstr): raise NotImplementedError - + def send_ctrl_to_process(self, letter): char = chr("abcdefghijklmnopqrstuvwxyz".index(letter) + 1) byte_array = QByteArray() @@ -295,38 +313,6 @@ self.process.write(byte_array) self.process.waitForBytesWritten(-1) self.shell.write(LOCALE_CODEC.toUnicode(byte_array), flush=True) - + def keyboard_interrupt(self): raise NotImplementedError - - -def test(): - from spyderlib.utils.qthelpers import qapplication - app = qapplication() - from spyderlib.widgets.externalshell.pythonshell import ExternalPythonShell - from spyderlib.widgets.externalshell.systemshell import ExternalSystemShell - import spyderlib - from spyderlib.plugins.variableexplorer import VariableExplorer - settings = VariableExplorer.get_settings() - shell = ExternalPythonShell(wdir=osp.dirname(spyderlib.__file__), - ipykernel=True, stand_alone=settings, - arguments="-q4thread -pylab -colors LightBG", - light_background=False) -# shell = ExternalPythonShell(wdir=osp.dirname(spyderlib.__file__), -# interact=True, umr_enabled=True, -# stand_alone=settings, -# umr_namelist=['guidata', 'guiqwt'], -# umr_verbose=True, light_background=False) -# shell = ExternalSystemShell(wdir=osp.dirname(spyderlib.__file__), -# light_background=False) - shell.shell.toggle_wrap_mode(True) - shell.start_shell(False) - from spyderlib.qt.QtGui import QFont - font = QFont("Lucida console") - font.setPointSize(10) - shell.shell.set_font(font) - shell.show() - sys.exit(app.exec_()) - -if __name__ == "__main__": - test() \ Kein Zeilenumbruch am Dateiende. diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/widgets/externalshell/__init__.py spyder-3.0.2+dfsg1/spyder/widgets/externalshell/__init__.py --- spyder-2.3.8+dfsg1/spyder/widgets/externalshell/__init__.py 2015-08-24 21:09:46.000000000 +0200 +++ spyder-3.0.2+dfsg1/spyder/widgets/externalshell/__init__.py 2016-10-25 02:05:23.000000000 +0200 @@ -1,12 +1,12 @@ # -*- coding: utf-8 -*- # -# Copyright © 2009-2010 Pierre Raybaut +# Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) +# (see spyder/__init__.py for details) """ -spyderlib.widgets.externalshell -=============================== +spyder.widgets.externalshell +============================ External Shell widget: execute Python script/terminal in a separate process """ diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/widgets/externalshell/inputhooks.py spyder-3.0.2+dfsg1/spyder/widgets/externalshell/inputhooks.py --- spyder-2.3.8+dfsg1/spyder/widgets/externalshell/inputhooks.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/widgets/externalshell/inputhooks.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,150 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Inputhook management for GUI event loop integration - -Copyright (C) The IPython Development Team -Distributed under the terms of the modified BSD license -""" - -# Stdlib imports -import ctypes -import os -import sys - -# Qt imports -if os.environ["QT_API"] == 'pyqt': - from PyQt4 import QtCore, QtGui -elif os.environ["QT_API"] == 'pyside': - from PySide import QtCore, QtGui # analysis:ignore - -#----------------------------------------------------------------------------- -# Utilities -#----------------------------------------------------------------------------- -def _stdin_ready_posix(): - """Return True if there's something to read on stdin (posix version).""" - infds, outfds, erfds = select.select([sys.stdin],[],[],0) - return bool(infds) - -def _stdin_ready_nt(): - """Return True if there's something to read on stdin (nt version).""" - return msvcrt.kbhit() - -def _stdin_ready_other(): - """Return True, assuming there's something to read on stdin.""" - return True - - -def _ignore_CTRL_C_posix(): - """Ignore CTRL+C (SIGINT).""" - signal.signal(signal.SIGINT, signal.SIG_IGN) - -def _allow_CTRL_C_posix(): - """Take CTRL+C into account (SIGINT).""" - signal.signal(signal.SIGINT, signal.default_int_handler) - -def _ignore_CTRL_C_other(): - """Ignore CTRL+C (not implemented).""" - pass - -def _allow_CTRL_C_other(): - """Take CTRL+C into account (not implemented).""" - pass - - -if os.name == 'posix': - import select - import signal - stdin_ready = _stdin_ready_posix - ignore_CTRL_C = _ignore_CTRL_C_posix - allow_CTRL_C = _allow_CTRL_C_posix -elif os.name == 'nt': - import msvcrt - stdin_ready = _stdin_ready_nt - ignore_CTRL_C = _ignore_CTRL_C_other - allow_CTRL_C = _allow_CTRL_C_other -else: - stdin_ready = _stdin_ready_other - ignore_CTRL_C = _ignore_CTRL_C_other - allow_CTRL_C = _allow_CTRL_C_other - - -def clear_inputhook(): - """Set PyOS_InputHook to NULL and return the previous one""" - pyos_inputhook_ptr = ctypes.c_void_p.in_dll(ctypes.pythonapi, - "PyOS_InputHook") - pyos_inputhook_ptr.value = ctypes.c_void_p(None).value - allow_CTRL_C() - -def get_pyos_inputhook(): - """Return the current PyOS_InputHook as a ctypes.c_void_p.""" - return ctypes.c_void_p.in_dll(ctypes.pythonapi, "PyOS_InputHook") - -def set_pyft_callback(callback): - callback = ctypes.PYFUNCTYPE(ctypes.c_int)(callback) - return callback - -def remove_pyqt_inputhook(): - if os.environ["QT_API"] == 'pyqt': - QtCore.pyqtRemoveInputHook() - -#------------------------------------------------------------------------------ -# Input hooks -#------------------------------------------------------------------------------ -def qt4(): - """PyOS_InputHook python hook for Qt4. - - Process pending Qt events and if there's no pending keyboard - input, spend a short slice of time (50ms) running the Qt event - loop. - - As a Python ctypes callback can't raise an exception, we catch - the KeyboardInterrupt and temporarily deactivate the hook, - which will let a *second* CTRL+C be processed normally and go - back to a clean prompt line. - """ - try: - allow_CTRL_C() - app = QtCore.QCoreApplication.instance() - if not app: - app = QtGui.QApplication([" "]) - app.processEvents(QtCore.QEventLoop.AllEvents, 300) - if not stdin_ready(): - # Generally a program would run QCoreApplication::exec() - # from main() to enter and process the Qt event loop until - # quit() or exit() is called and the program terminates. - # - # For our input hook integration, we need to repeatedly - # enter and process the Qt event loop for only a short - # amount of time (say 50ms) to ensure that Python stays - # responsive to other user inputs. - # - # A naive approach would be to repeatedly call - # QCoreApplication::exec(), using a timer to quit after a - # short amount of time. Unfortunately, QCoreApplication - # emits an aboutToQuit signal before stopping, which has - # the undesirable effect of closing all modal windows. - # - # To work around this problem, we instead create a - # QEventLoop and call QEventLoop::exec(). Other than - # setting some state variables which do not seem to be - # used anywhere, the only thing QCoreApplication adds is - # the aboutToQuit signal which is precisely what we are - # trying to avoid. - timer = QtCore.QTimer() - event_loop = QtCore.QEventLoop() - timer.timeout.connect(event_loop.quit) - while not stdin_ready(): - timer.start(50) - event_loop.exec_() - timer.stop() - except KeyboardInterrupt: - print("\nKeyboardInterrupt - Press Enter for new prompt") - except: # NO exceptions are allowed to escape from a ctypes callback - ignore_CTRL_C() - from traceback import print_exc - print_exc() - print("Got exception from inputhook, unregistering.") - clear_inputhook() - finally: - allow_CTRL_C() - return 0 diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/widgets/externalshell/introspection.py spyder-3.0.2+dfsg1/spyder/widgets/externalshell/introspection.py --- spyder-2.3.8+dfsg1/spyder/widgets/externalshell/introspection.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/widgets/externalshell/introspection.py 2016-10-25 02:05:23.000000000 +0200 @@ -1,23 +1,25 @@ # -*- coding: utf-8 -*- # -# Copyright © 2011 Pierre Raybaut +# Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) +# (see spyder/__init__.py for details) """External shell's introspection and notification servers""" -from spyderlib.qt.QtCore import QThread, SIGNAL, Signal - -import threading -import socket +# Standard library imports import errno import os +import socket +import threading + +# Third party imports +from qtpy.QtCore import QThread, Signal # Local imports -from spyderlib.baseconfig import get_conf_path, DEBUG -from spyderlib.utils.misc import select_port -from spyderlib.utils.debug import log_last_error -from spyderlib.utils.bsdsocket import read_packet, write_packet +from spyder.config.base import get_conf_path, DEBUG +from spyder.utils.debug import log_last_error +from spyder.utils.bsdsocket import read_packet, write_packet +from spyder.utils.misc import select_port LOG_FILENAME = get_conf_path('introspection.log') @@ -31,6 +33,7 @@ SPYDER_PORT = 20128 + class IntrospectionServer(threading.Thread): """Introspection server""" def __init__(self): @@ -138,6 +141,10 @@ class NotificationThread(QThread): """Notification thread""" sig_process_remote_view = Signal(object) + sig_pdb = Signal(str, int) + open_file = Signal(str, int) + refresh_namespace_browser = Signal() + def __init__(self): QThread.__init__(self) self.notify_socket = None @@ -173,17 +180,15 @@ data = cdict.get('data') if command == 'pdb_step': fname, lineno = data - self.emit(SIGNAL('pdb(QString,int)'), fname, lineno) - self.emit(SIGNAL('refresh_namespace_browser()')) + self.sig_pdb.emit(fname, lineno) + self.refresh_namespace_browser.emit() elif command == 'refresh': - self.emit(SIGNAL('refresh_namespace_browser()')) + self.refresh_namespace_browser.emit() elif command == 'remote_view': self.sig_process_remote_view.emit(data) - elif command == 'ipykernel': - self.emit(SIGNAL('new_ipython_kernel(QString)'), data) elif command == 'open_file': fname, lineno = data - self.emit(SIGNAL('open_file(QString,int)'), fname, lineno) + self.open_file.emit(fname, lineno) else: raise RuntimeError('Unsupported command: %r' % command) if DEBUG_INTROSPECTION: diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/widgets/externalshell/monitor.py spyder-3.0.2+dfsg1/spyder/widgets/externalshell/monitor.py --- spyder-2.3.8+dfsg1/spyder/widgets/externalshell/monitor.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/widgets/externalshell/monitor.py 2016-10-25 02:05:23.000000000 +0200 @@ -1,8 +1,13 @@ # -*- coding: utf-8 -*- +# +# Copyright © Spyder Project Contributors +# Licensed under the terms of the MIT License +# (see spyder/__init__.py for details) + """External shell's monitor""" -#TODO: The "disable auto-refresh when variable explorer is hidden" feature -# broken since we removed the "shell" widget reference from notification +#TODO: The "disable auto-refresh when variable explorer is hidden" feature +# broken since we removed the "shell" widget reference from notification # thread. We must find another mechanism to avoid refreshing systematically # remote views for all consoles...! @@ -12,77 +17,26 @@ import threading # Local imports -from spyderlib.utils.misc import fix_reference_name -from spyderlib.utils.debug import log_last_error -from spyderlib.utils.dochelpers import (getargtxt, getdoc, getsource, - getobjdir, isdefined) -from spyderlib.utils.bsdsocket import (communicate, read_packet, write_packet, - PACKET_NOT_RECEIVED, PICKLE_HIGHEST_PROTOCOL) -from spyderlib.utils.introspection.module_completion import module_completion -from spyderlib.baseconfig import get_conf_path, get_supported_types, DEBUG -from spyderlib.py3compat import getcwd, is_text_string, pickle, _thread - +from spyder.config.base import get_conf_path, DEBUG +from spyder.py3compat import getcwd, is_text_string, pickle, _thread +from spyder.utils.misc import fix_reference_name +from spyder.utils.debug import log_last_error +from spyder.utils.dochelpers import (getargtxt, getdoc, getsource, + getobjdir, isdefined) +from spyder.utils.bsdsocket import (communicate, read_packet, write_packet, + PACKET_NOT_RECEIVED, PICKLE_HIGHEST_PROTOCOL) +from spyder.utils.introspection.module_completion import module_completion +from spyder.widgets.variableexplorer.utils import (get_remote_data, + make_remote_view) -SUPPORTED_TYPES = {} LOG_FILENAME = get_conf_path('monitor.log') - DEBUG_MONITOR = DEBUG >= 2 - if DEBUG_MONITOR: import logging logging.basicConfig(filename=get_conf_path('monitor_debug.log'), level=logging.DEBUG) -REMOTE_SETTINGS = ('check_all', 'exclude_private', 'exclude_uppercase', - 'exclude_capitalized', 'exclude_unsupported', - 'excluded_names', 'truncate', 'minmax', - 'remote_editing', 'autorefresh') - -def get_remote_data(data, settings, mode, more_excluded_names=None): - """ - Return globals according to filter described in *settings*: - * data: data to be filtered (dictionary) - * settings: variable explorer settings (dictionary) - * mode (string): 'editable' or 'picklable' - * more_excluded_names: additional excluded names (list) - """ - from spyderlib.widgets.dicteditorutils import globalsfilter - global SUPPORTED_TYPES - if not SUPPORTED_TYPES: - SUPPORTED_TYPES = get_supported_types() - assert mode in list(SUPPORTED_TYPES.keys()) - excluded_names = settings['excluded_names'] - if more_excluded_names is not None: - excluded_names += more_excluded_names - return globalsfilter(data, check_all=settings['check_all'], - filters=tuple(SUPPORTED_TYPES[mode]), - exclude_private=settings['exclude_private'], - exclude_uppercase=settings['exclude_uppercase'], - exclude_capitalized=settings['exclude_capitalized'], - exclude_unsupported=settings['exclude_unsupported'], - excluded_names=excluded_names) - -def make_remote_view(data, settings, more_excluded_names=None): - """ - Make a remote view of dictionary *data* - -> globals explorer - """ - from spyderlib.widgets.dicteditorutils import (get_human_readable_type, - get_size, get_color_name, value_to_display) - assert all([name in REMOTE_SETTINGS for name in settings]) - data = get_remote_data(data, settings, mode='editable', - more_excluded_names=more_excluded_names) - remote = {} - for key, value in list(data.items()): - view = value_to_display(value, truncate=settings['truncate'], - minmax=settings['minmax']) - remote[key] = {'type': get_human_readable_type(value), - 'size': get_size(value), - 'color': get_color_name(value), - 'view': view} - return remote - def monitor_save_globals(sock, settings, filename): """Save globals() to file""" @@ -111,20 +65,17 @@ return communicate(sock, '__copy_global__("%s", "%s")' \ % (orig_name, new_name)) - def _getcdlistdir(): """Return current directory list dir""" return os.listdir(getcwd()) + class Monitor(threading.Thread): """Monitor server""" def __init__(self, host, introspection_port, notification_port, shell_id, timeout, auto_refresh): threading.Thread.__init__(self) self.setDaemon(True) - - self.ipykernel = None - self.ipython_shell = None self.pdb_obj = None @@ -137,9 +88,6 @@ self.inputhook_flag = False self.first_inputhook_call = True - # To grab the IPython internal namespace - self.ip = None - # Connecting to introspection server self.i_request = socket.socket( socket.AF_INET ) self.i_request.connect( (host, introspection_port) ) @@ -218,20 +166,10 @@ self._mglobals = glbs else: glbs = self._mglobals - if self.ipykernel is None and '__ipythonkernel__' in glbs: - self.ipykernel = glbs['__ipythonkernel__'] - communicate(self.n_request, - dict(command="ipykernel", - data=self.ipykernel.connection_file)) - if self.ipython_shell is None and '__ipythonshell__' in glbs: - # IPython kernel - self.ipython_shell = glbs['__ipythonshell__'] - glbs = self.ipython_shell.user_ns - self.ip = self.ipython_shell.get_ipython() self._mglobals = glbs return glbs - def get_current_namespace(self, with_magics=False): + def get_current_namespace(self): """Return current namespace, i.e. globals() if not debugging, or a dictionary containing both locals() and globals() for current frame when debugging""" @@ -243,15 +181,7 @@ else: ns.update(glbs) ns.update(self.pdb_locals) - - # Add magics to ns so we can show help about them on the Object - # Inspector - if self.ip and with_magics: - line_magics = self.ip.magics_manager.magics['line'] - cell_magics = self.ip.magics_manager.magics['cell'] - ns.update(line_magics) - ns.update(cell_magics) - + return ns def get_reference_namespace(self, name): @@ -274,7 +204,7 @@ def isdefined(self, obj, force_import=False): """Return True if object is defined in current namespace""" - ns = self.get_current_namespace(with_magics=True) + ns = self.get_current_namespace() return isdefined(obj, force_import=force_import, namespace=ns) def toggle_inputhook_flag(self, state): @@ -337,7 +267,7 @@ and *valid* is True if object evaluation did not raise any exception """ assert is_text_string(text) - ns = self.get_current_namespace(with_magics=True) + ns = self.get_current_namespace() try: return eval(text, ns), True except: @@ -397,7 +327,7 @@ """Return True if object is an instance of class PIL.Image.Image""" ns = self.get_current_namespace() try: - from spyderlib.pil_patch import Image + from spyder.pil_patch import Image return isinstance(ns[name], Image.Image) except ImportError: return False @@ -446,26 +376,23 @@ settings = self.remote_view_settings if settings: ns = self.get_current_namespace() - more_excluded_names = ['In', 'Out'] if self.ipython_shell else None - remote_view = make_remote_view(ns, settings, more_excluded_names) + remote_view = make_remote_view(ns, settings) communicate(self.n_request, dict(command="remote_view", data=remote_view)) def saveglobals(self): """Save globals() into filename""" ns = self.get_current_namespace() - from spyderlib.utils.iofuncs import iofunctions + from spyder.utils.iofuncs import iofunctions settings = read_packet(self.i_request) filename = read_packet(self.i_request) - more_excluded_names = ['In', 'Out'] if self.ipython_shell else None - data = get_remote_data(ns, settings, mode='picklable', - more_excluded_names=more_excluded_names).copy() + data = get_remote_data(ns, settings, mode='picklable').copy() return iofunctions.save(data, filename) def loadglobals(self): """Load globals() from filename""" glbs = self.mglobals() - from spyderlib.utils.iofuncs import iofunctions + from spyder.utils.iofuncs import iofunctions filename = read_packet(self.i_request) ext = read_packet(self.i_request) load_func = iofunctions.load_funcs[ext] @@ -514,7 +441,6 @@ self.refresh_after_eval = True def run(self): - self.ipython_shell = None while True: output = pickle.dumps(None, PICKLE_HIGHEST_PROTOCOL) glbs = self.mglobals() diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/widgets/externalshell/namespacebrowser.py spyder-3.0.2+dfsg1/spyder/widgets/externalshell/namespacebrowser.py --- spyder-2.3.8+dfsg1/spyder/widgets/externalshell/namespacebrowser.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/widgets/externalshell/namespacebrowser.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,561 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright © 2009-2010 Pierre Raybaut -# Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) - -"""Namespace browser widget""" - -import os.path as osp -import socket - -from spyderlib.qt.QtGui import (QWidget, QVBoxLayout, QHBoxLayout, QMenu, - QToolButton, QMessageBox, QApplication, - QCursor, QInputDialog) -from spyderlib.qt.QtCore import SIGNAL, Qt, Signal -from spyderlib.qt.compat import getopenfilenames, getsavefilename - -# Local imports -from spyderlib.widgets.externalshell.monitor import ( - monitor_set_global, monitor_get_global, monitor_del_global, - monitor_copy_global, monitor_save_globals, monitor_load_globals, - communicate, REMOTE_SETTINGS) -from spyderlib.widgets.dicteditor import (RemoteDictEditorTableView, - DictEditorTableView) -from spyderlib.widgets.dicteditorutils import globalsfilter -from spyderlib.utils import encoding -from spyderlib.utils.misc import fix_reference_name -from spyderlib.utils.programs import is_module_installed -from spyderlib.utils.qthelpers import (get_icon, create_toolbutton, - add_actions, create_action) -from spyderlib.utils.iofuncs import iofunctions -from spyderlib.widgets.importwizard import ImportWizard -from spyderlib.baseconfig import _, get_supported_types -from spyderlib.py3compat import is_text_string, to_text_string, getcwd - - -SUPPORTED_TYPES = get_supported_types() - - -class NamespaceBrowser(QWidget): - """Namespace browser (global variables explorer widget)""" - sig_option_changed = Signal(str, object) - def __init__(self, parent): - QWidget.__init__(self, parent) - - self.shellwidget = None - self.is_internal_shell = None - self.ipyclient = None - self.is_ipykernel = None - - self.is_visible = True # Do not modify: light mode won't work! - - self.setup_in_progress = None - - # Remote dict editor settings - self.check_all = None - self.exclude_private = None - self.exclude_uppercase = None - self.exclude_capitalized = None - self.exclude_unsupported = None - self.excluded_names = None - self.truncate = None - self.minmax = None - self.remote_editing = None - self.autorefresh = None - - self.editor = None - self.exclude_private_action = None - self.exclude_uppercase_action = None - self.exclude_capitalized_action = None - self.exclude_unsupported_action = None - - self.filename = None - - def setup(self, check_all=None, exclude_private=None, - exclude_uppercase=None, exclude_capitalized=None, - exclude_unsupported=None, excluded_names=None, - truncate=None, minmax=None, remote_editing=None, - autorefresh=None): - """Setup the namespace browser""" - assert self.shellwidget is not None - - self.check_all = check_all - self.exclude_private = exclude_private - self.exclude_uppercase = exclude_uppercase - self.exclude_capitalized = exclude_capitalized - self.exclude_unsupported = exclude_unsupported - self.excluded_names = excluded_names - self.truncate = truncate - self.minmax = minmax - self.remote_editing = remote_editing - self.autorefresh = autorefresh - - if self.editor is not None: - self.editor.setup_menu(truncate, minmax) - self.exclude_private_action.setChecked(exclude_private) - self.exclude_uppercase_action.setChecked(exclude_uppercase) - self.exclude_capitalized_action.setChecked(exclude_capitalized) - self.exclude_unsupported_action.setChecked(exclude_unsupported) - # Don't turn autorefresh on for IPython kernels - # See Issue 1450 - if not self.is_ipykernel: - self.auto_refresh_button.setChecked(autorefresh) - self.refresh_table() - return - - # Dict editor: - if self.is_internal_shell: - self.editor = DictEditorTableView(self, None, truncate=truncate, - minmax=minmax) - else: - self.editor = RemoteDictEditorTableView(self, None, - truncate=truncate, minmax=minmax, - remote_editing=remote_editing, - get_value_func=self.get_value, - set_value_func=self.set_value, - new_value_func=self.set_value, - remove_values_func=self.remove_values, - copy_value_func=self.copy_value, - is_list_func=self.is_list, - get_len_func=self.get_len, - is_array_func=self.is_array, - is_image_func=self.is_image, - is_dict_func=self.is_dict, - is_data_frame_func=self.is_data_frame, - is_series_func=self.is_series, - get_array_shape_func=self.get_array_shape, - get_array_ndim_func=self.get_array_ndim, - oedit_func=self.oedit, - plot_func=self.plot, imshow_func=self.imshow, - show_image_func=self.show_image) - self.editor.sig_option_changed.connect(self.sig_option_changed.emit) - self.editor.sig_files_dropped.connect(self.import_data) - - - # Setup layout - hlayout = QHBoxLayout() - vlayout = QVBoxLayout() - toolbar = self.setup_toolbar(exclude_private, exclude_uppercase, - exclude_capitalized, exclude_unsupported, - autorefresh) - vlayout.setAlignment(Qt.AlignTop) - for widget in toolbar: - vlayout.addWidget(widget) - hlayout.addWidget(self.editor) - hlayout.addLayout(vlayout) - self.setLayout(hlayout) - hlayout.setContentsMargins(0, 0, 0, 0) - - self.sig_option_changed.connect(self.option_changed) - - def set_shellwidget(self, shellwidget): - """Bind shellwidget instance to namespace browser""" - self.shellwidget = shellwidget - from spyderlib.widgets import internalshell - self.is_internal_shell = isinstance(self.shellwidget, - internalshell.InternalShell) - self.is_ipykernel = self.shellwidget.is_ipykernel - if not self.is_internal_shell: - shellwidget.set_namespacebrowser(self) - - def set_ipyclient(self, ipyclient): - """Bind ipyclient instance to namespace browser""" - self.ipyclient = ipyclient - - def setup_toolbar(self, exclude_private, exclude_uppercase, - exclude_capitalized, exclude_unsupported, autorefresh): - """Setup toolbar""" - self.setup_in_progress = True - - toolbar = [] - - refresh_button = create_toolbutton(self, text=_("Refresh"), - icon=get_icon('reload.png'), - triggered=self.refresh_table) - self.auto_refresh_button = create_toolbutton(self, - text=_("Refresh periodically"), - icon=get_icon('auto_reload.png'), - toggled=self.toggle_auto_refresh) - self.auto_refresh_button.setChecked(autorefresh) - load_button = create_toolbutton(self, text=_("Import data"), - icon=get_icon('fileimport.png'), - triggered=self.import_data) - self.save_button = create_toolbutton(self, text=_("Save data"), - icon=get_icon('filesave.png'), - triggered=lambda: self.save_data(self.filename)) - self.save_button.setEnabled(False) - save_as_button = create_toolbutton(self, - text=_("Save data as..."), - icon=get_icon('filesaveas.png'), - triggered=self.save_data) - toolbar += [refresh_button, self.auto_refresh_button, load_button, - self.save_button, save_as_button] - - self.exclude_private_action = create_action(self, - _("Exclude private references"), - tip=_("Exclude references which name starts" - " with an underscore"), - toggled=lambda state: - self.sig_option_changed.emit('exclude_private', state)) - self.exclude_private_action.setChecked(exclude_private) - - self.exclude_uppercase_action = create_action(self, - _("Exclude all-uppercase references"), - tip=_("Exclude references which name is uppercase"), - toggled=lambda state: - self.sig_option_changed.emit('exclude_uppercase', state)) - self.exclude_uppercase_action.setChecked(exclude_uppercase) - - self.exclude_capitalized_action = create_action(self, - _("Exclude capitalized references"), - tip=_("Exclude references which name starts with an " - "uppercase character"), - toggled=lambda state: - self.sig_option_changed.emit('exclude_capitalized', state)) - self.exclude_capitalized_action.setChecked(exclude_capitalized) - - self.exclude_unsupported_action = create_action(self, - _("Exclude unsupported data types"), - tip=_("Exclude references to unsupported data types" - " (i.e. which won't be handled/saved correctly)"), - toggled=lambda state: - self.sig_option_changed.emit('exclude_unsupported', state)) - self.exclude_unsupported_action.setChecked(exclude_unsupported) - - options_button = create_toolbutton(self, text=_("Options"), - icon=get_icon('tooloptions.png')) - toolbar.append(options_button) - options_button.setPopupMode(QToolButton.InstantPopup) - menu = QMenu(self) - editor = self.editor - actions = [self.exclude_private_action, self.exclude_uppercase_action, - self.exclude_capitalized_action, - self.exclude_unsupported_action, None, - editor.truncate_action] - if is_module_installed('numpy'): - actions.append(editor.minmax_action) - add_actions(menu, actions) - options_button.setMenu(menu) - - self.setup_in_progress = False - - return toolbar - - def option_changed(self, option, value): - """Option has changed""" - setattr(self, to_text_string(option), value) - if not self.is_internal_shell: - settings = self.get_view_settings() - communicate(self._get_sock(), - 'set_remote_view_settings()', settings=[settings]) - - def visibility_changed(self, enable): - """Notify the widget whether its container (the namespace browser - plugin is visible or not""" - # This is slowing down Spyder a lot if too much data is present in - # the Variable Explorer, and users give focus to it after being hidden. - # This also happens when the Variable Explorer is visible and users - # give focus to Spyder after using another application (like Chrome - # or Firefox). - # That's why we've decided to remove this feature - # Fixes Issue 2593 - # - # self.is_visible = enable - # if enable: - # self.refresh_table() - pass - - def toggle_auto_refresh(self, state): - """Toggle auto refresh state""" - self.autorefresh = state - if not self.setup_in_progress and not self.is_internal_shell: - communicate(self._get_sock(), - "set_monitor_auto_refresh(%r)" % state) - - def _get_sock(self): - """Return socket connection""" - return self.shellwidget.introspection_socket - - def get_internal_shell_filter(self, mode, check_all=None): - """ - Return internal shell data types filter: - * check_all: check all elements data types for sequences - (dict, list, tuple) - * mode (string): 'editable' or 'picklable' - """ - assert mode in list(SUPPORTED_TYPES.keys()) - if check_all is None: - check_all = self.check_all - def wsfilter(input_dict, check_all=check_all, - filters=tuple(SUPPORTED_TYPES[mode])): - """Keep only objects that can be pickled""" - return globalsfilter( - input_dict, check_all=check_all, filters=filters, - exclude_private=self.exclude_private, - exclude_uppercase=self.exclude_uppercase, - exclude_capitalized=self.exclude_capitalized, - exclude_unsupported=self.exclude_unsupported, - excluded_names=self.excluded_names) - return wsfilter - - def get_view_settings(self): - """Return dict editor view settings""" - settings = {} - for name in REMOTE_SETTINGS: - settings[name] = getattr(self, name) - return settings - - def refresh_table(self): - """Refresh variable table""" - if self.is_visible and self.isVisible(): - if self.is_internal_shell: - # Internal shell - wsfilter = self.get_internal_shell_filter('editable') - self.editor.set_filter(wsfilter) - interpreter = self.shellwidget.interpreter - if interpreter is not None: - self.editor.set_data(interpreter.namespace) - self.editor.adjust_columns() - elif self.shellwidget.is_running(): - # import time; print >>STDOUT, time.ctime(time.time()), "Refreshing namespace browser" - sock = self._get_sock() - if sock is None: - return - try: - communicate(sock, "refresh()") - except socket.error: - # Process was terminated before calling this method - pass - - def process_remote_view(self, remote_view): - """Process remote view""" - if remote_view is not None: - self.set_data(remote_view) - - #------ Remote Python process commands ------------------------------------ - def get_value(self, name): - value = monitor_get_global(self._get_sock(), name) - if value is None: - if communicate(self._get_sock(), '%s is not None' % name): - import pickle - msg = to_text_string(_("Object %s is not picklable") - % name) - raise pickle.PicklingError(msg) - return value - - def set_value(self, name, value): - monitor_set_global(self._get_sock(), name, value) - self.refresh_table() - - def remove_values(self, names): - for name in names: - monitor_del_global(self._get_sock(), name) - self.refresh_table() - - def copy_value(self, orig_name, new_name): - monitor_copy_global(self._get_sock(), orig_name, new_name) - self.refresh_table() - - def is_list(self, name): - """Return True if variable is a list or a tuple""" - return communicate(self._get_sock(), - 'isinstance(%s, (tuple, list))' % name) - - def is_dict(self, name): - """Return True if variable is a dictionary""" - return communicate(self._get_sock(), 'isinstance(%s, dict)' % name) - - def get_len(self, name): - """Return sequence length""" - return communicate(self._get_sock(), "len(%s)" % name) - - def is_array(self, name): - """Return True if variable is a NumPy array""" - return communicate(self._get_sock(), 'is_array("%s")' % name) - - def is_image(self, name): - """Return True if variable is a PIL.Image image""" - return communicate(self._get_sock(), 'is_image("%s")' % name) - - def is_data_frame(self, name): - """Return True if variable is a DataFrame""" - return communicate(self._get_sock(), - "isinstance(globals()['%s'], DataFrame)" % name) - - def is_series(self, name): - """Return True if variable is a Series""" - return communicate(self._get_sock(), - "isinstance(globals()['%s'], Series)" % name) - - def get_array_shape(self, name): - """Return array's shape""" - return communicate(self._get_sock(), "%s.shape" % name) - - def get_array_ndim(self, name): - """Return array's ndim""" - return communicate(self._get_sock(), "%s.ndim" % name) - - def plot(self, name, funcname): - command = "import spyderlib.pyplot; "\ - "__fig__ = spyderlib.pyplot.figure(); "\ - "__items__ = getattr(spyderlib.pyplot, '%s')(%s); "\ - "spyderlib.pyplot.show(); "\ - "del __fig__, __items__;" % (funcname, name) - if self.is_ipykernel: - self.ipyclient.shellwidget.execute("%%varexp --%s %s" % (funcname, - name)) - else: - self.shellwidget.send_to_process(command) - - def imshow(self, name): - command = "import spyderlib.pyplot; " \ - "__fig__ = spyderlib.pyplot.figure(); " \ - "__items__ = spyderlib.pyplot.imshow(%s); " \ - "spyderlib.pyplot.show(); del __fig__, __items__;" % name - if self.is_ipykernel: - self.ipyclient.shellwidget.execute("%%varexp --imshow %s" % name) - else: - self.shellwidget.send_to_process(command) - - def show_image(self, name): - command = "%s.show()" % name - if self.is_ipykernel: - self.ipyclient.shellwidget.execute(command) - else: - self.shellwidget.send_to_process(command) - - def oedit(self, name): - command = "from spyderlib.widgets.objecteditor import oedit; " \ - "oedit('%s', modal=False, namespace=locals());" % name - self.shellwidget.send_to_process(command) - - #------ Set, load and save data ------------------------------------------- - def set_data(self, data): - """Set data""" - if data != self.editor.model.get_data(): - self.editor.set_data(data) - self.editor.adjust_columns() - - def collapse(self): - """Collapse""" - self.emit(SIGNAL('collapse()')) - - def import_data(self, filenames=None): - """Import data from text file""" - title = _("Import data") - if filenames is None: - if self.filename is None: - basedir = getcwd() - else: - basedir = osp.dirname(self.filename) - filenames, _selfilter = getopenfilenames(self, title, basedir, - iofunctions.load_filters) - if not filenames: - return - elif is_text_string(filenames): - filenames = [filenames] - - - for filename in filenames: - - self.filename = to_text_string(filename) - ext = osp.splitext(self.filename)[1].lower() - - if ext not in iofunctions.load_funcs: - buttons = QMessageBox.Yes | QMessageBox.Cancel - answer = QMessageBox.question(self, title, - _("Unsupported file extension '%s'

    " - "Would you like to import it anyway " - "(by selecting a known file format)?" - ) % ext, buttons) - if answer == QMessageBox.Cancel: - return - formats = list(iofunctions.load_extensions.keys()) - item, ok = QInputDialog.getItem(self, title, - _('Open file as:'), - formats, 0, False) - if ok: - ext = iofunctions.load_extensions[to_text_string(item)] - else: - return - - load_func = iofunctions.load_funcs[ext] - - # 'import_wizard' (self.setup_io) - if is_text_string(load_func): - # Import data with import wizard - error_message = None - try: - text, _encoding = encoding.read(self.filename) - if self.is_internal_shell: - self.editor.import_from_string(text) - else: - base_name = osp.basename(self.filename) - editor = ImportWizard(self, text, title=base_name, - varname=fix_reference_name(base_name)) - if editor.exec_(): - var_name, clip_data = editor.get_data() - monitor_set_global(self._get_sock(), - var_name, clip_data) - except Exception as error: - error_message = str(error) - else: - QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) - QApplication.processEvents() - if self.is_internal_shell: - namespace, error_message = load_func(self.filename) - interpreter = self.shellwidget.interpreter - for key in list(namespace.keys()): - new_key = fix_reference_name(key, - blacklist=list(interpreter.namespace.keys())) - if new_key != key: - namespace[new_key] = namespace.pop(key) - if error_message is None: - interpreter.namespace.update(namespace) - else: - error_message = monitor_load_globals(self._get_sock(), - self.filename, ext) - QApplication.restoreOverrideCursor() - QApplication.processEvents() - - if error_message is not None: - QMessageBox.critical(self, title, - _("Unable to load '%s'" - "

    Error message:
    %s" - ) % (self.filename, error_message)) - self.refresh_table() - - - def save_data(self, filename=None): - """Save data""" - if filename is None: - filename = self.filename - if filename is None: - filename = getcwd() - filename, _selfilter = getsavefilename(self, _("Save data"), - filename, - iofunctions.save_filters) - if filename: - self.filename = filename - else: - return False - QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) - QApplication.processEvents() - if self.is_internal_shell: - wsfilter = self.get_internal_shell_filter('picklable', - check_all=True) - namespace = wsfilter(self.shellwidget.interpreter.namespace).copy() - error_message = iofunctions.save(namespace, filename) - else: - settings = self.get_view_settings() - error_message = monitor_save_globals(self._get_sock(), - settings, filename) - QApplication.restoreOverrideCursor() - QApplication.processEvents() - if error_message is not None: - QMessageBox.critical(self, _("Save data"), - _("Unable to save current workspace" - "

    Error message:
    %s") % error_message) - self.save_button.setEnabled(self.filename is not None) - diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/widgets/externalshell/osx_app_site.py spyder-3.0.2+dfsg1/spyder/widgets/externalshell/osx_app_site.py --- spyder-2.3.8+dfsg1/spyder/widgets/externalshell/osx_app_site.py 2015-08-24 21:09:46.000000000 +0200 +++ spyder-3.0.2+dfsg1/spyder/widgets/externalshell/osx_app_site.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,163 +0,0 @@ -# -# IMPORTANT NOTE: Don't add a coding line here! It's not necessary for -# site files -# -# Spyder's MacOS X App site.py additions -# -# It includes missing variables and paths that are not added by -# py2app to its own site.py -# -# These functions were taken verbatim from Python 2.7.3 site.py -# - -import sys -import os -try: - import __builtin__ as builtins -except ImportError: - # Python 3 - import builtins - -# for distutils.commands.install -# These values are initialized by the getuserbase() and getusersitepackages() -# functions. -USER_SITE = None -USER_BASE = None - - -def getuserbase(): - """Returns the `user base` directory path. - - The `user base` directory can be used to store data. If the global - variable ``USER_BASE`` is not initialized yet, this function will also set - it. - """ - global USER_BASE - if USER_BASE is not None: - return USER_BASE - from sysconfig import get_config_var - USER_BASE = get_config_var('userbase') - return USER_BASE - -def getusersitepackages(): - """Returns the user-specific site-packages directory path. - - If the global variable ``USER_SITE`` is not initialized yet, this - function will also set it. - """ - global USER_SITE - user_base = getuserbase() # this will also set USER_BASE - - if USER_SITE is not None: - return USER_SITE - - from sysconfig import get_path - - if sys.platform == 'darwin': - from sysconfig import get_config_var - if get_config_var('PYTHONFRAMEWORK'): - USER_SITE = get_path('purelib', 'osx_framework_user') - return USER_SITE - - USER_SITE = get_path('purelib', '%s_user' % os.name) - return USER_SITE - - -class _Printer(object): - """interactive prompt objects for printing the license text, a list of - contributors and the copyright notice.""" - - MAXLINES = 23 - - def __init__(self, name, data, files=(), dirs=()): - self.__name = name - self.__data = data - self.__files = files - self.__dirs = dirs - self.__lines = None - - def __setup(self): - if self.__lines: - return - data = None - for dir in self.__dirs: - for filename in self.__files: - filename = os.path.join(dir, filename) - try: - fp = open(filename, "rU") - data = fp.read() - fp.close() - break - except IOError: - pass - if data: - break - if not data: - data = self.__data - self.__lines = data.split('\n') - self.__linecnt = len(self.__lines) - - def __repr__(self): - self.__setup() - if len(self.__lines) <= self.MAXLINES: - return "\n".join(self.__lines) - else: - return "Type %s() to see the full %s text" % ((self.__name,)*2) - - def __call__(self): - self.__setup() - prompt = 'Hit Return for more, or q (and Return) to quit: ' - lineno = 0 - while 1: - try: - for i in range(lineno, lineno + self.MAXLINES): - print(self.__lines[i]) - except IndexError: - break - else: - lineno += self.MAXLINES - key = None - while key is None: - try: - key = raw_input(prompt) - except NameError: - # Python 3 - key = input(prompt) - if key not in ('', 'q'): - key = None - if key == 'q': - break - -def setcopyright(): - """Set 'copyright' and 'credits' in builtins""" - builtins.copyright = _Printer("copyright", sys.copyright) - if sys.platform[:4] == 'java': - builtins.credits = _Printer( - "credits", - "Jython is maintained by the Jython developers (www.jython.org).") - else: - builtins.credits = _Printer("credits", """\ - Thanks to CWI, CNRI, BeOpen.com, Zope Corporation and a cast of thousands - for supporting Python development. See www.python.org for more information.""") - here = os.path.dirname(os.__file__) - builtins.license = _Printer( - "license", "See http://www.python.org/%.3s/license.html" % sys.version, - ["LICENSE.txt", "LICENSE"], - [os.path.join(here, os.pardir), here, os.curdir]) - - -class _Helper(object): - """Define the builtin 'help'. - This is a wrapper around pydoc.help (with a twist). - - """ - - def __repr__(self): - return "Type help() for interactive help, " \ - "or help(object) for help about object." - def __call__(self, *args, **kwds): - import pydoc - return pydoc.help(*args, **kwds) - -def sethelper(): - builtins.help = _Helper() diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/widgets/externalshell/pythonshell.py spyder-3.0.2+dfsg1/spyder/widgets/externalshell/pythonshell.py --- spyder-2.3.8+dfsg1/spyder/widgets/externalshell/pythonshell.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/widgets/externalshell/pythonshell.py 2016-10-25 02:05:23.000000000 +0200 @@ -1,40 +1,46 @@ # -*- coding: utf-8 -*- # -# Copyright © 2009-2010 Pierre Raybaut +# Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) +# (see spyder/__init__.py for details) """External Python Shell widget: execute Python script in a separate process""" -import sys +# Standard library imports import os import os.path as osp import socket +import sys -from spyderlib.qt.QtGui import QApplication, QMessageBox, QSplitter, QMenu -from spyderlib.qt.QtCore import QProcess, SIGNAL, Qt -from spyderlib.qt.compat import getexistingdirectory +# Third party imports +from qtpy.compat import getexistingdirectory +from qtpy.QtCore import QProcess, QProcessEnvironment, Qt, Signal, Slot +from qtpy.QtWidgets import QApplication, QMenu, QMessageBox, QSplitter # Local imports -from spyderlib.utils.qthelpers import (get_icon, get_std_icon, add_actions, - create_toolbutton, create_action, - DialogManager) -from spyderlib.utils.environ import RemoteEnvDialog -from spyderlib.utils.programs import get_python_args -from spyderlib.utils.misc import get_python_executable -from spyderlib.baseconfig import (_, get_module_source_path, DEBUG, - MAC_APP_NAME, running_in_mac_app) -from spyderlib.widgets.shell import PythonShellWidget -from spyderlib.widgets.externalshell.namespacebrowser import NamespaceBrowser -from spyderlib.utils.bsdsocket import communicate, write_packet -from spyderlib.widgets.externalshell.baseshell import (ExternalShellBase, - add_pathlist_to_PYTHONPATH) -from spyderlib.widgets.dicteditor import DictEditor -from spyderlib.py3compat import (is_text_string, to_text_string, - to_binary_string) +from spyder.config.base import (_, DEBUG, get_module_source_path, + MAC_APP_NAME, running_in_mac_app) +from spyder.py3compat import (is_text_string, to_binary_string, + to_text_string) +from spyder.utils import icon_manager as ima +from spyder.utils.bsdsocket import communicate, write_packet +from spyder.utils.environ import RemoteEnvDialog +from spyder.utils.misc import add_pathlist_to_PYTHONPATH, get_python_executable +from spyder.utils.programs import get_python_args +from spyder.utils.qthelpers import (add_actions, create_action, + create_toolbutton, DialogManager) +from spyder.widgets.externalshell.baseshell import ExternalShellBase +from spyder.widgets.shell import PythonShellWidget +from spyder.widgets.variableexplorer.namespacebrowser import NamespaceBrowser +from spyder.widgets.variableexplorer.collectionseditor import CollectionsEditor class ExtPythonShellWidget(PythonShellWidget): + + wait_for_ready_read = Signal() + go_to_error = Signal(str) + focus_changed = Signal() + def __init__(self, parent, history_filename, profile=False): PythonShellWidget.__init__(self, parent, history_filename, profile) self.path = [] @@ -46,7 +52,7 @@ def clear_terminal(self): """Reimplement ShellBaseWidget method""" self.clear() - self.emit(SIGNAL("execute(QString)"), "\n") + self.execute.emit("\n") def execute_lines(self, lines): """ @@ -66,7 +72,7 @@ import time time.sleep(0.025) else: - self.emit(SIGNAL("wait_for_ready_read()")) + self.wait_for_ready_read.emit() self.flush() #------ Code completion / Calltips @@ -148,41 +154,48 @@ def set_spyder_breakpoints(self): """Set Spyder breakpoints into debugging session""" return self.ask_monitor("set_spyder_breakpoints()") + + def is_running(self): + """Check if parent is running""" + return self.parent().is_running() class ExternalPythonShell(ExternalShellBase): """External Shell widget: execute Python script in a separate process""" SHELL_CLASS = ExtPythonShellWidget + sig_pdb = Signal(str, int) + open_file = Signal(str, int) + started = Signal() + sig_finished = Signal() + def __init__(self, parent=None, fname=None, wdir=None, - interact=False, debug=False, path=[], python_args='', - ipykernel=False, arguments='', stand_alone=None, + interact=False, debug=False, post_mortem=False, + path=[], python_args='', + arguments='', stand_alone=None, umr_enabled=True, umr_namelist=[], umr_verbose=True, pythonstartup=None, pythonexecutable=None, + external_interpreter=False, monitor_enabled=True, mpl_backend=None, ets_backend='qt4', - qt_api=None, pyqt_api=0, - ignore_sip_setapi_errors=False, merge_output_channels=False, + qt_api=None, merge_output_channels=False, colorize_sys_stderr=False, autorefresh_timeout=3000, autorefresh_state=True, light_background=True, menu_actions=None, show_buttons_inside=True, show_elapsed_time=True): - assert qt_api in (None, 'pyqt', 'pyside') + assert qt_api in (None, 'pyqt', 'pyside', 'pyqt5') self.namespacebrowser = None # namespace browser widget! - self.dialog_manager = DialogManager() - + self.stand_alone = stand_alone # stand alone settings (None: plugin) self.interact = interact - self.is_ipykernel = ipykernel self.pythonstartup = pythonstartup self.pythonexecutable = pythonexecutable + self.external_interpreter = external_interpreter self.monitor_enabled = monitor_enabled self.mpl_backend = mpl_backend self.ets_backend = ets_backend self.qt_api = qt_api - self.pyqt_api = pyqt_api - self.ignore_sip_setapi_errors = ignore_sip_setapi_errors self.merge_output_channels = merge_output_channels self.colorize_sys_stderr = colorize_sys_stderr self.umr_enabled = umr_enabled @@ -213,30 +226,25 @@ if python_args: assert is_text_string(python_args) self.python_args = python_args - + assert is_text_string(arguments) self.arguments = arguments - - self.connection_file = None - if self.is_ipykernel: - self.interact = False - # Running our custom startup script for IPython kernels: - # (see spyderlib/widgets/externalshell/start_ipython_kernel.py) - self.fname = get_module_source_path( - 'spyderlib.widgets.externalshell', 'start_ipython_kernel.py') - + self.connection_file = None self.shell.set_externalshell(self) self.toggle_globals_explorer(False) self.interact_action.setChecked(self.interact) self.debug_action.setChecked(debug) + self.introspection_socket = None self.is_interpreter = fname is None if self.is_interpreter: self.terminate_button.hide() + + self.post_mortem_action.setChecked(post_mortem and not self.is_interpreter) # Additional python path list self.path = path @@ -266,12 +274,12 @@ if self.namespacebrowser_button is None \ and self.stand_alone is not None: self.namespacebrowser_button = create_toolbutton(self, - text=_("Variables"), icon=get_icon('dictedit.png'), + text=_("Variables"), icon=ima.icon('dictedit'), tip=_("Show/hide global variables explorer"), toggled=self.toggle_globals_explorer, text_beside_icon=True) if self.terminate_button is None: self.terminate_button = create_toolbutton(self, - text=_("Terminate"), icon=get_icon('stop.png'), + text=_("Terminate"), icon=ima.icon('stop'), tip=_("Attempts to stop the process. The process\n" "may not exit as a result of clicking this\n" "button (it is given the chance to prompt\n" @@ -291,19 +299,22 @@ self.debug_action.setCheckable(True) self.args_action = create_action(self, _("Arguments..."), triggered=self.get_arguments) + self.post_mortem_action = create_action(self, _("Post Mortem Debug")) + self.post_mortem_action.setCheckable(True) run_settings_menu = QMenu(_("Run settings"), self) add_actions(run_settings_menu, - (self.interact_action, self.debug_action, self.args_action)) + (self.interact_action, self.debug_action, self.args_action, + self.post_mortem_action)) self.cwd_button = create_action(self, _("Working directory"), - icon=get_std_icon('DirOpenIcon'), + icon=ima.icon('DirOpenIcon'), tip=_("Set current working directory"), triggered=self.set_current_working_directory) self.env_button = create_action(self, _("Environment variables"), - icon=get_icon('environ.png'), + icon=ima.icon('environ'), triggered=self.show_env) self.syspath_button = create_action(self, _("Show sys.path contents"), - icon=get_icon('syspath.png'), + icon=ima.icon('syspath'), triggered=self.show_syspath) actions = [run_settings_menu, self.show_time_action, None, self.cwd_button, self.env_button, self.syspath_button] @@ -323,12 +334,11 @@ settings = self.stand_alone self.namespacebrowser.set_shellwidget(self) self.namespacebrowser.setup(**settings) - self.connect(self.namespacebrowser, SIGNAL('collapse()'), + self.namespacebrowser.sig_collapse.connect( lambda: self.toggle_globals_explorer(False)) # Shell splitter self.splitter = splitter = QSplitter(Qt.Vertical, self) - self.connect(self.splitter, SIGNAL('splitterMoved(int, int)'), - self.splitter_moved) + self.splitter.splitterMoved.connect(self.splitter_moved) splitter.addWidget(self.shell) splitter.setCollapsible(0, False) splitter.addWidget(self.namespacebrowser) @@ -339,13 +349,14 @@ return splitter def get_icon(self): - return get_icon('python.png') + return ima.icon('python') def set_buttons_runnning_state(self, state): ExternalShellBase.set_buttons_runnning_state(self, state) self.interact_action.setEnabled(not state and not self.is_interpreter) self.debug_action.setEnabled(not state and not self.is_interpreter) self.args_action.setEnabled(not state and not self.is_interpreter) + self.post_mortem_action.setEnabled(not state and not self.is_interpreter) if state: if self.arguments: argstr = _("Arguments: %s") % self.arguments @@ -373,8 +384,7 @@ def configure_namespacebrowser(self): """Connect the namespace browser to the notification thread""" if self.notification_thread is not None: - self.connect(self.notification_thread, - SIGNAL('refresh_namespace_browser()'), + self.notification_thread.refresh_namespace_browser.connect( self.namespacebrowser.refresh_table) signal = self.notification_thread.sig_process_remote_view signal.connect(lambda data: @@ -388,14 +398,14 @@ self.process.setProcessChannelMode(QProcess.MergedChannels) else: self.process.setProcessChannelMode(QProcess.SeparateChannels) - self.connect(self.shell, SIGNAL("wait_for_ready_read()"), + self.shell.wait_for_ready_read.connect( lambda: self.process.waitForReadyRead(250)) # Working directory if self.wdir is not None: self.process.setWorkingDirectory(self.wdir) - #-------------------------Python specific------------------------------- + #-------------------------Python specific------------------------------ # Python arguments p_args = ['-u'] if DEBUG >= 3: @@ -410,45 +420,42 @@ if self.pythonstartup: env.append('PYTHONSTARTUP=%s' % self.pythonstartup) + #-------------------------Python specific------------------------------- + # Post mortem debugging + if self.post_mortem_action.isChecked(): + env.append('SPYDER_EXCEPTHOOK=True') + # Set standard input/output encoding for Python consoles - # (IPython handles it on its own) # See http://stackoverflow.com/q/26312400/438386, specifically # the comments of Martijn Pieters - if not self.is_ipykernel: - env.append('PYTHONIOENCODING=UTF-8') - + env.append('PYTHONIOENCODING=UTF-8') + # Monitor if self.monitor_enabled: env.append('SPYDER_SHELL_ID=%s' % id(self)) env.append('SPYDER_AR_TIMEOUT=%d' % self.autorefresh_timeout) env.append('SPYDER_AR_STATE=%r' % self.autorefresh_state) - from spyderlib.widgets.externalshell import introspection + from spyder.widgets.externalshell import introspection introspection_server = introspection.start_introspection_server() introspection_server.register(self) notification_server = introspection.start_notification_server() self.notification_thread = notification_server.register(self) - self.connect(self.notification_thread, SIGNAL('pdb(QString,int)'), - lambda fname, lineno: - self.emit(SIGNAL('pdb(QString,int)'), fname, lineno)) - self.connect(self.notification_thread, - SIGNAL('new_ipython_kernel(QString)'), - lambda args: - self.emit(SIGNAL('create_ipython_client(QString)'), - args)) - self.connect(self.notification_thread, - SIGNAL('open_file(QString,int)'), - lambda fname, lineno: - self.emit(SIGNAL('open_file(QString,int)'), - fname, lineno)) + self.notification_thread.sig_pdb.connect( + lambda fname, lineno: + self.sig_pdb.emit(fname, lineno)) + self.notification_thread.open_file.connect( + lambda fname, lineno: + self.open_file.emit(fname, lineno)) if self.namespacebrowser is not None: self.configure_namespacebrowser() env.append('SPYDER_I_PORT=%d' % introspection_server.port) env.append('SPYDER_N_PORT=%d' % notification_server.port) - + # External modules options env.append('ETS_TOOLKIT=%s' % self.ets_backend) - if self.mpl_backend: - env.append('MATPLOTLIB_BACKEND=%s' % self.mpl_backend) + if self.mpl_backend is not None: + backends = {0: 'Automatic', 1: 'None', 2: 'TkAgg'} + env.append('SPY_MPL_BACKEND=%s' % backends[self.mpl_backend]) if self.qt_api: env.append('QT_API=%s' % self.qt_api) env.append('COLORIZE_SYS_STDERR=%s' % self.colorize_sys_stderr) @@ -457,11 +464,7 @@ # from PyQt4.QtNetwork import QLocalServer # self.local_server = QLocalServer() # self.local_server.listen(str(id(self))) - if self.pyqt_api: - env.append('PYQT_API=%d' % self.pyqt_api) - env.append('IGNORE_SIP_SETAPI_ERRORS=%s' - % self.ignore_sip_setapi_errors) - + # User Module Deleter if self.is_interpreter: env.append('UMR_ENABLED=%r' % self.umr_enabled) @@ -474,13 +477,14 @@ else: env.append('MATPLOTLIB_ION=False') - # IPython kernel - env.append('IPYTHON_KERNEL=%r' % self.is_ipykernel) + # External interpreter + env.append('EXTERNAL_INTERPRETER=%r' % self.external_interpreter) - # Add sitecustomize path to path list + # Add sitecustomize path to path list pathlist = [] - scpath = osp.dirname(osp.abspath(__file__)) - pathlist.append(scpath) + spy_path = get_module_source_path('spyder') + sc_path = osp.join(spy_path, 'utils', 'site') + pathlist.append(sc_path) # Adding Spyder path pathlist += self.path @@ -488,23 +492,17 @@ # Adding path list to PYTHONPATH environment variable add_pathlist_to_PYTHONPATH(env, pathlist) - #-------------------------Python specific------------------------------- + #-------------------------Python specific------------------------------ - self.connect(self.process, SIGNAL("readyReadStandardOutput()"), - self.write_output) - self.connect(self.process, SIGNAL("readyReadStandardError()"), - self.write_error) - self.connect(self.process, SIGNAL("finished(int,QProcess::ExitStatus)"), - self.finished) - - self.connect(self, SIGNAL('finished()'), self.dialog_manager.close_all) - - self.connect(self.terminate_button, SIGNAL("clicked()"), - self.process.terminate) - self.connect(self.kill_button, SIGNAL("clicked()"), - self.process.kill) + self.process.readyReadStandardOutput.connect(self.write_output) + self.process.readyReadStandardError.connect(self.write_error) + self.process.finished.connect(lambda ec, es=QProcess.ExitStatus: + self.finished(ec, es)) + self.sig_finished.connect(self.dialog_manager.close_all) + self.terminate_button.clicked.connect(self.process.terminate) + self.kill_button.clicked.connect(self.process.kill) - #-------------------------Python specific------------------------------- + #-------------------------Python specific------------------------------ # Fixes for our Mac app: # 1. PYTHONPATH and PYTHONHOME are set while bootstrapping the app, # but their values are messing sys.path for external interpreters @@ -514,7 +512,6 @@ # 3. Remove PYTHONOPTIMIZE from env so that we can have assert # statements working with our interpreters (See Issue 1281) if running_in_mac_app(): - env.append('SPYDER_INTERPRETER=%s' % self.pythonexecutable) if MAC_APP_NAME not in self.pythonexecutable: env = [p for p in env if not (p.startswith('PYTHONPATH') or \ p.startswith('PYTHONHOME'))] # 1. @@ -522,37 +519,33 @@ add_pathlist_to_PYTHONPATH(env, pathlist, drop_env=True) # 2. env = [p for p in env if not p.startswith('PYTHONOPTIMIZE')] # 3. - self.process.setEnvironment(env) + processEnvironment = QProcessEnvironment() + for envItem in env: + envName, separator, envValue = envItem.partition('=') + processEnvironment.insert(envName, envValue) + self.process.setProcessEnvironment(processEnvironment) self.process.start(self.pythonexecutable, p_args) - #-------------------------Python specific------------------------------- + #-------------------------Python specific------------------------------ running = self.process.waitForStarted(3000) self.set_running_state(running) if not running: - if self.is_ipykernel: - self.emit(SIGNAL("ipython_kernel_start_error(QString)"), - _("The kernel failed to start!! That's all we know... " - "Please close this console and open a new one.")) - else: - QMessageBox.critical(self, _("Error"), - _("A Python console failed to start!")) + QMessageBox.critical(self, _("Error"), + _("A Python console failed to start!")) else: self.shell.setFocus() - self.emit(SIGNAL('started()')) + self.started.emit() return self.process def finished(self, exit_code, exit_status): """Reimplement ExternalShellBase method""" - if self.is_ipykernel and exit_code == 1: - self.emit(SIGNAL("ipython_kernel_start_error(QString)"), - self.shell.get_text_with_eol()) ExternalShellBase.finished(self, exit_code, exit_status) self.introspection_socket = None -#=============================================================================== +#============================================================================== # Input/Output -#=============================================================================== +#============================================================================== def write_error(self): if os.name == 'nt': #---This is apparently necessary only on Windows (not sure though): @@ -566,13 +559,11 @@ def send_to_process(self, text): if not self.is_running(): return - if not is_text_string(text): text = to_text_string(text) - if self.mpl_backend == 'Qt4Agg' and os.name == 'nt' and \ + if self.mpl_backend == 0 and os.name == 'nt' and \ self.introspection_socket is not None: - communicate(self.introspection_socket, - "toggle_inputhook_flag(True)") + communicate(self.introspection_socket, "toggle_inputhook_flag(True)") # # Socket-based alternative (see input hook in sitecustomize.py): # while self.local_server.hasPendingConnections(): # self.local_server.nextPendingConnection().write('go!') @@ -600,9 +591,10 @@ except socket.error: pass -#=============================================================================== +#============================================================================== # Globals explorer -#=============================================================================== +#============================================================================== + @Slot(bool) def toggle_globals_explorer(self, state): if self.stand_alone is not None: self.splitter.setSizes([1, 1 if state else 0]) @@ -613,27 +605,59 @@ def splitter_moved(self, pos, index): self.namespacebrowser_button.setChecked( self.splitter.sizes()[1] ) -#=============================================================================== +#============================================================================== # Misc. -#=============================================================================== +#============================================================================== + @Slot() def set_current_working_directory(self): """Set current working directory""" cwd = self.shell.get_cwd() - self.emit(SIGNAL('redirect_stdio(bool)'), False) + self.redirect_stdio.emit(False) directory = getexistingdirectory(self, _("Select directory"), cwd) if directory: self.shell.set_cwd(directory) - self.emit(SIGNAL('redirect_stdio(bool)'), True) + self.redirect_stdio.emit(True) + @Slot() def show_env(self): """Show environment variables""" get_func = self.shell.get_env set_func = self.shell.set_env self.dialog_manager.show(RemoteEnvDialog(get_func, set_func)) - + + @Slot() def show_syspath(self): """Show sys.path contents""" - editor = DictEditor() + editor = CollectionsEditor() editor.setup(self.shell.get_syspath(), title="sys.path", readonly=True, - width=600, icon='syspath.png') + width=600, icon=ima.icon('syspath')) self.dialog_manager.show(editor) + + +def test(): + from spyder.utils.qthelpers import qapplication + app = qapplication() + + from spyder.plugins.variableexplorer import VariableExplorer + settings = VariableExplorer.get_settings() + + shell = ExternalPythonShell(pythonexecutable=sys.executable, + interact=True, + stand_alone=settings, + wdir=osp.dirname(__file__), + mpl_backend=0, + light_background=False) + + from spyder.config.gui import get_font + + font = get_font() + shell.shell.set_font(font) + + shell.shell.toggle_wrap_mode(True) + shell.start_shell(False) + shell.show() + sys.exit(app.exec_()) + + +if __name__ == "__main__": + test() \ Kein Zeilenumbruch am Dateiende. diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/widgets/externalshell/sitecustomize.py spyder-3.0.2+dfsg1/spyder/widgets/externalshell/sitecustomize.py --- spyder-2.3.8+dfsg1/spyder/widgets/externalshell/sitecustomize.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/widgets/externalshell/sitecustomize.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,778 +0,0 @@ -# -# IMPORTANT NOTE: Don't add a coding line here! It's not necessary for -# site files -# -# Spyder's ExternalPythonShell sitecustomize -# - -import sys -import os -import os.path as osp -import pdb -import bdb - - -PY2 = sys.version[0] == '2' - - -#============================================================================== -# sys.argv can be missing when Python is embedded, taking care of it. -# Fixes Issue 1473 and other crazy crashes with IPython 0.13 trying to -# access it. -#============================================================================== -if not hasattr(sys, 'argv'): - sys.argv = [''] - - -#============================================================================== -# Important Note: -# -# We avoid importing spyderlib here, so we are handling Python 3 compatiblity -# by hand. -#============================================================================== -def _print(*objects, **options): - end = options.get('end', '\n') - file = options.get('file', sys.stdout) - sep = options.get('sep', ' ') - string = sep.join([str(obj) for obj in objects]) - if sys.version[0] == '3': - # Python 3 - local_dict = {} - exec('printf = print', local_dict) # to avoid syntax error in Python 2 - local_dict['printf'](string, file=file, end=end, sep=sep) - else: - # Python 2 - if end: - print >>file, string - else: - print >>file, string, - - -#============================================================================== -# Execfile functions -# -# The definitions for Python 2 on Windows were taken from the IPython -# project (present in IPython.utils.py3compat) -# Copyright (C) The IPython Development Team -# Distributed under the terms of the modified BSD license -#============================================================================== -try: - # Python 2 - import __builtin__ as builtins - if os.name == 'nt': - def encode(u): - return u.encode('utf8', 'replace') - def execfile(fname, glob=None, loc=None): - loc = loc if (loc is not None) else glob - scripttext = builtins.open(fname).read()+ '\n' - # compile converts unicode filename to str assuming - # ascii. Let's do the conversion before calling compile - if isinstance(fname, unicode): - filename = encode(fname) - else: - filename = fname - exec(compile(scripttext, filename, 'exec'), glob, loc) - else: - def execfile(fname, *where): - if isinstance(fname, unicode): - filename = fname.encode(sys.getfilesystemencoding()) - else: - filename = fname - builtins.execfile(filename, *where) -except ImportError: - # Python 3 - import builtins - basestring = (str,) - def execfile(filename, namespace): - # Open a source file correctly, whatever its encoding is - exec(compile(open(filename, 'rb').read(), filename, 'exec'), namespace) - - -#============================================================================== -# Colorization of sys.stderr (standard Python interpreter) -#============================================================================== -if os.environ.get("COLORIZE_SYS_STDERR", "").lower() == "true": - class StderrProxy(object): - """Proxy to sys.stderr file object overriding only the `write` method - to provide red colorization for the whole stream, and blue-underlined - for traceback file links""" - def __init__(self): - self.old_stderr = sys.stderr - self.__buffer = '' - sys.stderr = self - - def __getattr__(self, name): - return getattr(self.old_stderr, name) - - def write(self, text): - if os.name == 'nt' and '\n' not in text: - self.__buffer += text - return - for text in (self.__buffer+text).splitlines(True): - if text.startswith(' File') \ - and not text.startswith(' File "<'): - # Show error links in blue underlined text - colored_text = ' '+'\x1b[4;34m'+text[2:]+'\x1b[0m' - else: - # Show error messages in red - colored_text = '\x1b[31m'+text+'\x1b[0m' - self.old_stderr.write(colored_text) - self.__buffer = '' - - stderrproxy = StderrProxy() - - -#============================================================================== -# Prepending this spyderlib package's path to sys.path to be sure -# that another version of spyderlib won't be imported instead: -#============================================================================== -spyderlib_path = osp.dirname(__file__) -while not osp.isdir(osp.join(spyderlib_path, 'spyderlib')): - spyderlib_path = osp.abspath(osp.join(spyderlib_path, os.pardir)) -if not spyderlib_path.startswith(sys.prefix): - # Spyder is not installed: moving its parent directory to the top of - # sys.path to be sure that this spyderlib package will be imported in - # the remote process (instead of another installed version of Spyder) - while spyderlib_path in sys.path: - sys.path.remove(spyderlib_path) - sys.path.insert(0, spyderlib_path) -os.environ['SPYDER_PARENT_DIR'] = spyderlib_path - - -#============================================================================== -# Set PyQt4 API to #1 or #2 -#============================================================================== -pyqt_api = int(os.environ.get("PYQT_API", "0")) -if pyqt_api: - try: - import sip - try: - for qtype in ('QString', 'QVariant'): - sip.setapi(qtype, pyqt_api) - except AttributeError: - # Old version of sip - pass - except ImportError: - pass - - -#============================================================================== -# Setting console encoding (otherwise Python does not recognize encoding) -# for Windows platforms -#============================================================================== -if os.name == 'nt': - try: - import locale, ctypes - _t, _cp = locale.getdefaultlocale('LANG') - try: - _cp = int(_cp[2:]) - ctypes.windll.kernel32.SetConsoleCP(_cp) - ctypes.windll.kernel32.SetConsoleOutputCP(_cp) - except (ValueError, TypeError): - # Code page number in locale is not valid - pass - except ImportError: - pass - - -#============================================================================== -# Settings for our MacOs X app -#============================================================================== -if sys.platform == 'darwin': - from spyderlib.baseconfig import MAC_APP_NAME - if MAC_APP_NAME in __file__: - interpreter = os.environ.get('SPYDER_INTERPRETER') - if MAC_APP_NAME not in interpreter: - # Add a minimal library (with spyderlib) at the end of sys.path to - # be able to connect our monitor to the external console - py_ver = '%s.%s' % (sys.version_info[0], sys.version_info[1]) - app_pythonpath = '%s/Contents/Resources/lib/python%s' % (MAC_APP_NAME, - py_ver) - full_pythonpath = [p for p in sys.path if p.endswith(app_pythonpath)] - if full_pythonpath: - sys.path.remove(full_pythonpath[0]) - sys.path.append(full_pythonpath[0] + osp.sep + 'minimal-lib') - else: - # Add missing variables and methods to the app's site module - import site - import osx_app_site - osx_app_site.setcopyright() - osx_app_site.sethelper() - site._Printer = osx_app_site._Printer - site.USER_BASE = osx_app_site.getuserbase() - site.USER_SITE = osx_app_site.getusersitepackages() - - -#============================================================================== -# Importing user's sitecustomize -#============================================================================== -try: - import sitecustomize #analysis:ignore -except ImportError: - pass - - -#============================================================================== -# Add default filesystem encoding on Linux to avoid an error with -# Matplotlib 1.5 in Python 2 (Fixes Issue 2793) -#============================================================================== -if PY2 and sys.platform.startswith('linux'): - def _getfilesystemencoding_wrapper(): - return 'utf-8' - - sys.getfilesystemencoding = _getfilesystemencoding_wrapper - - -#============================================================================== -# Importing matplotlib before creating the monitor. -# This prevents a kernel crash with the inline backend in our IPython -# consoles on Linux and Python 3 (Fixes Issue 2257) -#============================================================================== -try: - import matplotlib -except ImportError: - matplotlib = None # analysis:ignore - -#============================================================================== -# Communication between Spyder and the remote process -#============================================================================== -if os.environ.get('SPYDER_SHELL_ID') is None: - monitor = None -else: - from spyderlib.widgets.externalshell.monitor import Monitor - monitor = Monitor("127.0.0.1", - int(os.environ['SPYDER_I_PORT']), - int(os.environ['SPYDER_N_PORT']), - os.environ['SPYDER_SHELL_ID'], - float(os.environ['SPYDER_AR_TIMEOUT']), - os.environ["SPYDER_AR_STATE"].lower() == "true") - monitor.start() - - def open_in_spyder(source, lineno=1): - """ - Open a source file in Spyder's editor (it could be a filename or a - Python module/package). - - If you want to use IPython's %edit use %ed instead - """ - try: - source = sys.modules[source] - except KeyError: - source = source - if not isinstance(source, basestring): - try: - source = source.__file__ - except AttributeError: - raise ValueError("source argument must be either " - "a string or a module object") - if source.endswith('.pyc'): - source = source[:-1] - source = osp.abspath(source) - if osp.exists(source): - monitor.notify_open_file(source, lineno=lineno) - else: - _print("Can't open file %s" % source, file=sys.stderr) - builtins.open_in_spyder = open_in_spyder - - if os.environ["QT_API"] == 'pyqt': - from PyQt4 import QtCore - elif os.environ["QT_API"] == 'pyside': - from PySide import QtCore #analysis:ignore - - def qt_nt_inputhook(): - """Qt input hook for Windows - - This input hook wait for available stdin data (notified by - ExternalPythonShell through the monitor's inputhook_flag - attribute), and in the meantime it processes Qt events. - """ - # Refreshing variable explorer, except on first input hook call: - # (otherwise, on slow machines, this may freeze Spyder) - monitor.refresh_from_inputhook() - if os.name == 'nt': - try: - # This call fails for Python without readline support - # (or on Windows platforms) when PyOS_InputHook is called - # for the second consecutive time, because the 100-bytes - # stdin buffer is full. - # For more details, see the `PyOS_StdioReadline` function - # in Python source code (Parser/myreadline.c) - sys.stdin.tell() - except IOError: - return 0 - app = QtCore.QCoreApplication.instance() - if app and app.thread() is QtCore.QThread.currentThread(): - timer = QtCore.QTimer() - QtCore.QObject.connect(timer, QtCore.SIGNAL('timeout()'), - app, QtCore.SLOT('quit()')) - monitor.toggle_inputhook_flag(False) - while not monitor.inputhook_flag: - timer.start(50) - QtCore.QCoreApplication.exec_() - timer.stop() -# # Socket-based alternative: -# socket = QtNetwork.QLocalSocket() -# socket.connectToServer(os.environ['SPYDER_SHELL_ID']) -# socket.waitForConnected(-1) -# while not socket.waitForReadyRead(10): -# timer.start(50) -# QtCore.QCoreApplication.exec_() -# timer.stop() -# socket.read(3) -# socket.disconnectFromServer() - return 0 - - -#============================================================================== -# Matplotlib settings -#============================================================================== -if matplotlib is not None: - mpl_backend = os.environ.get("MATPLOTLIB_BACKEND", "") - mpl_ion = os.environ.get("MATPLOTLIB_ION", "") - if not mpl_backend: - mpl_backend = 'Qt4Agg' - - # To have mpl docstrings as rst - matplotlib.rcParams['docstring.hardcopy'] = True - - # Activate interactive mode when needed - if mpl_ion.lower() == "true": - matplotlib.rcParams['interactive'] = True - - if os.environ.get("IPYTHON_KERNEL", "").lower() != "true": - import ctypes - from spyderlib.widgets.externalshell import inputhooks - - # Setting the user defined backend - matplotlib.use(mpl_backend) - - # Setting the right input hook according to mpl_backend, - # IMPORTANT NOTE: Don't try to abstract the steps to set a PyOS - # input hook callback in a function. It will *crash* the - # interpreter!! - if mpl_backend == "Qt4Agg" and os.name == 'nt' and \ - monitor is not None: - # Removing PyQt4 input hook which is not working well on - # Windows since opening a subprocess does not attach a real - # console to it (with keyboard events...) - if os.environ["QT_API"] == 'pyqt': - inputhooks.remove_pyqt_inputhook() - # Using our own input hook - # NOTE: it's not working correctly for some configurations - # (See issue 1831) - callback = inputhooks.set_pyft_callback(qt_nt_inputhook) - pyos_ih = inputhooks.get_pyos_inputhook() - pyos_ih.value = ctypes.cast(callback, ctypes.c_void_p).value - elif mpl_backend == "Qt4Agg" and os.environ["QT_API"] == 'pyside': - # PySide doesn't have an input hook, so we need to install one - # to be able to show plots. - # Note: This only works well for Posix systems - callback = inputhooks.set_pyft_callback(inputhooks.qt4) - pyos_ih = inputhooks.get_pyos_inputhook() - pyos_ih.value = ctypes.cast(callback, ctypes.c_void_p).value - elif mpl_backend != "Qt4Agg" and os.environ["QT_API"] == 'pyqt': - # Matplotlib backends install their own input hooks, so we - # need to remove the PyQt one to make them work - inputhooks.remove_pyqt_inputhook() - - -#============================================================================== -# IPython adjustments -#============================================================================== -if os.environ.get("IPYTHON_KERNEL", "").lower() == "true": - - # Use ipydb as the debugger to patch on IPython consoles - from IPython.core.debugger import Pdb as ipyPdb - pdb.Pdb = ipyPdb - - # Patch unittest.main so that errors are printed directly in the console. - # See http://comments.gmane.org/gmane.comp.python.ipython.devel/10557 - # Fixes Issue 1370 - import unittest - from unittest import TestProgram - class IPyTesProgram(TestProgram): - def __init__(self, *args, **kwargs): - test_runner = unittest.TextTestRunner(stream=sys.stderr) - kwargs['testRunner'] = kwargs.pop('testRunner', test_runner) - kwargs['exit'] = False - TestProgram.__init__(self, *args, **kwargs) - unittest.main = IPyTesProgram - - # Pandas monkey-patches - try: - # Make Pandas recognize our IPython consoles as proper qtconsoles - # Fixes Issue 2015 - def in_qtconsole(): - return True - import pandas as pd - pd.core.common.in_qtconsole = in_qtconsole - - # Set Pandas output encoding - pd.options.display.encoding = 'utf-8' - except (ImportError, AttributeError): - pass - - -#============================================================================== -# Pdb adjustments -#============================================================================== -class SpyderPdb(pdb.Pdb): - def set_spyder_breakpoints(self): - self.clear_all_breaks() - #------Really deleting all breakpoints: - for bp in bdb.Breakpoint.bpbynumber: - if bp: - bp.deleteMe() - bdb.Breakpoint.next = 1 - bdb.Breakpoint.bplist = {} - bdb.Breakpoint.bpbynumber = [None] - #------ - from spyderlib.config import CONF - CONF.load_from_ini() - if CONF.get('run', 'breakpoints/enabled', True): - breakpoints = CONF.get('run', 'breakpoints', {}) - i = 0 - for fname, data in list(breakpoints.items()): - for linenumber, condition in data: - i += 1 - self.set_break(self.canonic(fname), linenumber, - cond=condition) - - def notify_spyder(self, frame): - if not frame: - return - fname = self.canonic(frame.f_code.co_filename) - if sys.version[0] == '2': - try: - fname = unicode(fname, "utf-8") - except TypeError: - pass - lineno = frame.f_lineno - if isinstance(fname, basestring) and isinstance(lineno, int): - if osp.isfile(fname) and monitor is not None: - monitor.notify_pdb_step(fname, lineno) - -pdb.Pdb = SpyderPdb - -#XXX: I know, this function is now also implemented as is in utils/misc.py but -# I'm kind of reluctant to import spyderlib in sitecustomize, even if this -# import is very clean. -def monkeypatch_method(cls, patch_name): - # This function's code was inspired from the following thread: - # "[Python-Dev] Monkeypatching idioms -- elegant or ugly?" - # by Robert Brewer - # (Tue Jan 15 19:13:25 CET 2008) - """ - Add the decorated method to the given class; replace as needed. - - If the named method already exists on the given class, it will - be replaced, and a reference to the old method is created as - cls._old. If the "_old__" attribute - already exists, KeyError is raised. - """ - def decorator(func): - fname = func.__name__ - old_func = getattr(cls, fname, None) - if old_func is not None: - # Add the old func to a list of old funcs. - old_ref = "_old_%s_%s" % (patch_name, fname) - #print(old_ref, old_func) - old_attr = getattr(cls, old_ref, None) - if old_attr is None: - setattr(cls, old_ref, old_func) - else: - raise KeyError("%s.%s already exists." - % (cls.__name__, old_ref)) - setattr(cls, fname, func) - return func - return decorator - -@monkeypatch_method(pdb.Pdb, 'Pdb') -def user_return(self, frame, return_value): - """This function is called when a return trap is set here.""" - # This is useful when debugging in an active interpreter (otherwise, - # the debugger will stop before reaching the target file) - if self._wait_for_mainpyfile: - if (self.mainpyfile != self.canonic(frame.f_code.co_filename) - or frame.f_lineno<= 0): - return - self._wait_for_mainpyfile = 0 - self._old_Pdb_user_return(frame, return_value) - -@monkeypatch_method(pdb.Pdb, 'Pdb') -def interaction(self, frame, traceback): - self.setup(frame, traceback) - self.notify_spyder(frame) #-----Spyder-specific------------------------- - self.print_stack_entry(self.stack[self.curindex]) - self.cmdloop() - self.forget() - -@monkeypatch_method(pdb.Pdb, 'Pdb') -def reset(self): - self._old_Pdb_reset() - if monitor is not None: - monitor.register_pdb_session(self) - self.set_spyder_breakpoints() - -#XXX: notify spyder on any pdb command (is that good or too lazy? i.e. is more -# specific behaviour desired?) -@monkeypatch_method(pdb.Pdb, 'Pdb') -def postcmd(self, stop, line): - self.notify_spyder(self.curframe) - return self._old_Pdb_postcmd(stop, line) - -# Breakpoints don't work for files with non-ascii chars in Python 2 -# Fixes Issue 1484 -if sys.version[0] == '2': - @monkeypatch_method(pdb.Pdb, 'Pdb') - def break_here(self, frame): - from bdb import effective - filename = self.canonic(frame.f_code.co_filename) - try: - filename = unicode(filename, "utf-8") - except TypeError: - pass - if not filename in self.breaks: - return False - lineno = frame.f_lineno - if not lineno in self.breaks[filename]: - # The line itself has no breakpoint, but maybe the line is the - # first line of a function with breakpoint set by function name. - lineno = frame.f_code.co_firstlineno - if not lineno in self.breaks[filename]: - return False - - # flag says ok to delete temp. bp - (bp, flag) = effective(filename, lineno, frame) - if bp: - self.currentbp = bp.number - if (flag and bp.temporary): - self.do_clear(str(bp.number)) - return True - else: - return False - - -#============================================================================== -# # Restoring (almost) original sys.path: -# (Note: do not remove spyderlib_path from sys.path because if Spyder has been -# installed using python setup.py install, then this could remove the -# 'site-packages' directory from sys.path!) -#============================================================================== -try: - sys.path.remove(osp.join(spyderlib_path, - "spyderlib", "widgets", "externalshell")) -except ValueError: - pass - - -#============================================================================== -# Ignore PyQt4's sip API changes (this should be used wisely -e.g. for -# debugging- as dynamic API change is not supported by PyQt) -#============================================================================== -if os.environ.get("IGNORE_SIP_SETAPI_ERRORS", "").lower() == "true": - try: - import sip - from sip import setapi as original_setapi - def patched_setapi(name, no): - try: - original_setapi(name, no) - except ValueError as msg: - _print("Warning/PyQt4-Spyder (%s)" % str(msg), file=sys.stderr) - sip.setapi = patched_setapi - except ImportError: - pass - - -#============================================================================== -# User module reloader -#============================================================================== -class UserModuleReloader(object): - """ - User Module Reloader (UMR) aims at deleting user modules - to force Python to deeply reload them during import - - pathlist [list]: blacklist in terms of module path - namelist [list]: blacklist in terms of module name - """ - def __init__(self, namelist=None, pathlist=None): - if namelist is None: - namelist = [] - spy_modules = ['sitecustomize', 'spyderlib', 'spyderplugins'] - mpl_modules = ['matplotlib', 'tkinter', 'Tkinter', 'gtk'] - self.namelist = namelist + spy_modules + mpl_modules - - if pathlist is None: - pathlist = [] - self.pathlist = pathlist - self.previous_modules = list(sys.modules.keys()) - - def is_module_blacklisted(self, modname, modpath): - for path in [sys.prefix]+self.pathlist: - if modpath.startswith(path): - return True - else: - return set(modname.split('.')) & set(self.namelist) - - def run(self, verbose=False): - """ - Del user modules to force Python to deeply reload them - - Do not del modules which are considered as system modules, i.e. - modules installed in subdirectories of Python interpreter's binary - Do not del C modules - """ - log = [] - for modname, module in list(sys.modules.items()): - if modname not in self.previous_modules: - modpath = getattr(module, '__file__', None) - if modpath is None: - # *module* is a C module that is statically linked into the - # interpreter. There is no way to know its path, so we - # choose to ignore it. - continue - if not self.is_module_blacklisted(modname, modpath): - log.append(modname) - del sys.modules[modname] - if verbose and log: - _print("\x1b[4;33m%s\x1b[24m%s\x1b[0m"\ - % ("Reloaded modules", ": "+", ".join(log))) - -__umr__ = None - - -#============================================================================== -# runfile and debugfile commands -#============================================================================== -def _get_globals(): - """Return current Python interpreter globals namespace""" - from __main__ import __dict__ as namespace - shell = namespace.get('__ipythonshell__') - if shell is not None and hasattr(shell, 'user_ns'): - # IPython 0.13+ kernel - return shell.user_ns - else: - # Python interpreter - return namespace - return namespace - - -def runfile(filename, args=None, wdir=None, namespace=None): - """ - Run filename - args: command line arguments (string) - wdir: working directory - """ - try: - filename = filename.decode('utf-8') - except (UnicodeError, TypeError, AttributeError): - # UnicodeError, TypeError --> eventually raised in Python 2 - # AttributeError --> systematically raised in Python 3 - pass - global __umr__ - if os.environ.get("UMR_ENABLED", "").lower() == "true": - if __umr__ is None: - namelist = os.environ.get("UMR_NAMELIST", None) - if namelist is not None: - namelist = namelist.split(',') - __umr__ = UserModuleReloader(namelist=namelist) - else: - verbose = os.environ.get("UMR_VERBOSE", "").lower() == "true" - __umr__.run(verbose=verbose) - if args is not None and not isinstance(args, basestring): - raise TypeError("expected a character buffer object") - if namespace is None: - namespace = _get_globals() - namespace['__file__'] = filename - sys.argv = [filename] - if args is not None: - for arg in args.split(): - sys.argv.append(arg) - if wdir is not None: - try: - wdir = wdir.decode('utf-8') - except (UnicodeError, TypeError, AttributeError): - # UnicodeError, TypeError --> eventually raised in Python 2 - # AttributeError --> systematically raised in Python 3 - pass - os.chdir(wdir) - execfile(filename, namespace) - sys.argv = [''] - namespace.pop('__file__') - -builtins.runfile = runfile - - -def debugfile(filename, args=None, wdir=None): - """ - Debug filename - args: command line arguments (string) - wdir: working directory - """ - debugger = pdb.Pdb() - filename = debugger.canonic(filename) - debugger._wait_for_mainpyfile = 1 - debugger.mainpyfile = filename - debugger._user_requested_quit = 0 - if os.name == 'nt': - filename = filename.replace('\\', '/') - debugger.run("runfile(%r, args=%r, wdir=%r)" % (filename, args, wdir)) - -builtins.debugfile = debugfile - - -#============================================================================== -# Evaluate external commands -#============================================================================== -def evalsc(command): - """Evaluate special commands - (analog to IPython's magic commands but far less powerful/complete)""" - assert command.startswith('%') - - from subprocess import Popen, PIPE - namespace = _get_globals() - command = command[1:].strip() # Remove leading % - - import re - clear_match = re.match(r"^clear ([a-zA-Z0-9_, ]+)", command) - cd_match = re.match(r"^cd \"?\'?([a-zA-Z0-9_\ \:\\\/\.]+)", command) - - if cd_match: - os.chdir(eval('r"%s"' % cd_match.groups()[0].strip())) - elif clear_match: - varnames = clear_match.groups()[0].replace(' ', '').split(',') - for varname in varnames: - try: - namespace.pop(varname) - except KeyError: - pass - elif command in ('cd', 'pwd'): - try: - _print(os.getcwdu()) - except AttributeError: - _print(os.getcwd()) - elif command == 'ls': - if os.name == 'nt': - Popen('dir', shell=True, stdin=PIPE) - _print('\n') - else: - Popen('ls', shell=True, stdin=PIPE) - _print('\n') - elif command == 'scientific': - from spyderlib import baseconfig - execfile(baseconfig.SCIENTIFIC_STARTUP, namespace) - else: - raise NotImplementedError("Unsupported command: '%s'" % command) - -builtins.evalsc = evalsc - - -#============================================================================== -# Restoring original PYTHONPATH -#============================================================================== -try: - os.environ['PYTHONPATH'] = os.environ['OLD_PYTHONPATH'] - del os.environ['OLD_PYTHONPATH'] -except KeyError: - if os.environ.get('PYTHONPATH') is not None: - del os.environ['PYTHONPATH'] diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/widgets/externalshell/start_ipython_kernel.py spyder-3.0.2+dfsg1/spyder/widgets/externalshell/start_ipython_kernel.py --- spyder-2.3.8+dfsg1/spyder/widgets/externalshell/start_ipython_kernel.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/widgets/externalshell/start_ipython_kernel.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,205 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright © 2009-2012 Pierre Raybaut -# Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) - -"""Startup file used by ExternalPythonShell exclusively for IPython kernels -(see spyderlib/widgets/externalshell/pythonshell.py)""" - -import sys -import os.path as osp - -# TODO: Move to Jupyter imports in 3.1 -try: - import warnings - from IPython.utils.shimmodule import ShimWarning - warnings.simplefilter('ignore', ShimWarning) -except: - pass - - -def sympy_config(mpl_backend): - """Sympy configuration""" - if mpl_backend is not None: - lines = """ -from sympy.interactive import init_session -init_session() -%matplotlib {0} -""".format(mpl_backend) - else: - lines = """ -from sympy.interactive import init_session -init_session() -""" - - return lines - - -def kernel_config(): - """Create a config object with IPython kernel options""" - from IPython.config.loader import Config, load_pyconfig_files - from IPython.core.application import get_ipython_dir - from spyderlib.config import CONF - from spyderlib.utils.programs import is_module_installed - - # ---- IPython config ---- - try: - profile_path = osp.join(get_ipython_dir(), 'profile_default') - ip_cfg = load_pyconfig_files(['ipython_config.py', - 'ipython_qtconsole_config.py'], - profile_path) - except: - ip_cfg = Config() - - # ---- Spyder config ---- - spy_cfg = Config() - - # Until we implement Issue 1052 - spy_cfg.InteractiveShell.xmode = 'Plain' - - # Run lines of code at startup - run_lines_o = CONF.get('ipython_console', 'startup/run_lines') - if run_lines_o: - spy_cfg.IPKernelApp.exec_lines = [x.strip() for x in run_lines_o.split(',')] - else: - spy_cfg.IPKernelApp.exec_lines = [] - - # Pylab configuration - mpl_backend = None - mpl_installed = is_module_installed('matplotlib') - pylab_o = CONF.get('ipython_console', 'pylab') - - if mpl_installed and pylab_o: - # Get matplotlib backend - backend_o = CONF.get('ipython_console', 'pylab/backend', 0) - backends = {0: 'inline', 1: 'auto', 2: 'qt', 3: 'osx', 4: 'gtk', - 5: 'wx', 6: 'tk'} - mpl_backend = backends[backend_o] - - # Automatically load Pylab and Numpy, or only set Matplotlib - # backend - autoload_pylab_o = CONF.get('ipython_console', 'pylab/autoload') - if autoload_pylab_o: - spy_cfg.IPKernelApp.exec_lines.append( - "%pylab {0}".format(mpl_backend)) - else: - spy_cfg.IPKernelApp.exec_lines.append( - "%matplotlib {0}".format(mpl_backend)) - - # Inline backend configuration - if backends[backend_o] == 'inline': - # Figure format - format_o = CONF.get('ipython_console', - 'pylab/inline/figure_format', 0) - formats = {0: 'png', 1: 'svg'} - spy_cfg.InlineBackend.figure_format = formats[format_o] - - # Resolution - spy_cfg.InlineBackend.rc = {'figure.figsize': (6.0, 4.0), - 'savefig.dpi': 72, - 'font.size': 10, - 'figure.subplot.bottom': .125, - 'figure.facecolor': 'white', - 'figure.edgecolor': 'white' - } - resolution_o = CONF.get('ipython_console', - 'pylab/inline/resolution') - spy_cfg.InlineBackend.rc['savefig.dpi'] = resolution_o - - # Figure size - width_o = float(CONF.get('ipython_console', 'pylab/inline/width')) - height_o = float(CONF.get('ipython_console', 'pylab/inline/height')) - spy_cfg.InlineBackend.rc['figure.figsize'] = (width_o, height_o) - - # Run a file at startup - use_file_o = CONF.get('ipython_console', 'startup/use_run_file') - run_file_o = CONF.get('ipython_console', 'startup/run_file') - if use_file_o and run_file_o: - spy_cfg.IPKernelApp.file_to_run = run_file_o - - # Autocall - autocall_o = CONF.get('ipython_console', 'autocall') - spy_cfg.ZMQInteractiveShell.autocall = autocall_o - - # To handle the banner by ourselves in IPython 3+ - spy_cfg.ZMQInteractiveShell.banner1 = '' - - # Greedy completer - greedy_o = CONF.get('ipython_console', 'greedy_completer') - spy_cfg.IPCompleter.greedy = greedy_o - - # Sympy loading - sympy_o = CONF.get('ipython_console', 'symbolic_math') - if sympy_o: - lines = sympy_config(mpl_backend) - spy_cfg.IPKernelApp.exec_lines.append(lines) - - # Merge IPython and Spyder configs. Spyder prefs will have prevalence - # over IPython ones - ip_cfg._merge(spy_cfg) - return ip_cfg - - -def change_edit_magic(shell): - """Use %edit to open files in Spyder""" - try: - shell.magics_manager.magics['line']['ed'] = \ - shell.magics_manager.magics['line']['edit'] - shell.magics_manager.magics['line']['edit'] = open_in_spyder #analysis:ignore - except: - pass - -def varexp(line): - """ - Spyder's variable explorer magic - - Used to generate plots, histograms and images of the variables displayed - on it. - """ - ip = get_ipython() #analysis:ignore - funcname, name = line.split() - import spyderlib.pyplot - __fig__ = spyderlib.pyplot.figure(); - __items__ = getattr(spyderlib.pyplot, funcname[2:])(ip.user_ns[name]) - spyderlib.pyplot.show() - del __fig__, __items__ - -# Remove this module's path from sys.path: -try: - sys.path.remove(osp.dirname(__file__)) -except ValueError: - pass - -locals().pop('__file__') -__doc__ = '' -__name__ = '__main__' - -# Add current directory to sys.path (like for any standard Python interpreter -# executed in interactive mode): -sys.path.insert(0, '') - -# Fire up the kernel instance. -from IPython.kernel.zmq.kernelapp import IPKernelApp -ipk_temp = IPKernelApp.instance() -ipk_temp.config = kernel_config() -ipk_temp.initialize() - -# Grabbing the kernel's shell to share its namespace with our -# Variable Explorer -__ipythonshell__ = ipk_temp.shell - -# Issue 977 : Since kernel.initialize() has completed execution, -# we can now allow the monitor to communicate the availablility of -# the kernel to accept front end connections. -__ipythonkernel__ = ipk_temp -del ipk_temp - -# Change %edit to open files inside Spyder -# NOTE: Leave this and other magic modifications *after* setting -# __ipythonkernel__ to not have problems while starting kernels -change_edit_magic(__ipythonshell__) -__ipythonshell__.register_magic_function(varexp) - -# Start the (infinite) kernel event loop. -__ipythonkernel__.start() diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/widgets/externalshell/systemshell.py spyder-3.0.2+dfsg1/spyder/widgets/externalshell/systemshell.py --- spyder-2.3.8+dfsg1/spyder/widgets/externalshell/systemshell.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/widgets/externalshell/systemshell.py 2016-10-25 02:05:23.000000000 +0200 @@ -1,31 +1,37 @@ # -*- coding: utf-8 -*- # -# Copyright © 2009-2010 Pierre Raybaut +# Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) +# (see spyder/__init__.py for details) """External System Shell widget: execute terminal in a separate process""" +# Standard library imports import os +import sys -from spyderlib.qt.QtGui import QMessageBox -from spyderlib.qt.QtCore import QProcess, SIGNAL, QTextCodec -LOCALE_CODEC = QTextCodec.codecForLocale() -CP850_CODEC = QTextCodec.codecForName('cp850') +# Third party imports +from qtpy.QtCore import QProcess, QProcessEnvironment, QTextCodec, Signal +from qtpy.QtWidgets import QMessageBox # Local imports -from spyderlib.utils.programs import shell_split -from spyderlib.baseconfig import _ -from spyderlib.utils.qthelpers import get_icon -from spyderlib.widgets.externalshell.baseshell import (ExternalShellBase, - add_pathlist_to_PYTHONPATH) -from spyderlib.widgets.shell import TerminalWidget -from spyderlib.py3compat import to_text_string, is_text_string +from spyder.config.base import _ +from spyder.py3compat import is_text_string, to_text_string +from spyder.utils import icon_manager as ima +from spyder.utils.misc import add_pathlist_to_PYTHONPATH +from spyder.widgets.externalshell.baseshell import ExternalShellBase +from spyder.widgets.shell import TerminalWidget + + +LOCALE_CODEC = QTextCodec.codecForLocale() +CP850_CODEC = QTextCodec.codecForName('cp850') class ExternalSystemShell(ExternalShellBase): """External Shell widget: execute Python script in a separate process""" SHELL_CLASS = TerminalWidget + started = Signal() + def __init__(self, parent=None, wdir=None, path=[], light_background=True, menu_actions=None, show_buttons_inside=True, show_elapsed_time=True): @@ -35,29 +41,39 @@ menu_actions=menu_actions, show_buttons_inside=show_buttons_inside, show_elapsed_time=show_elapsed_time) - + # Additional python path list self.path = path - + # For compatibility with the other shells that can live in the external # console - self.is_ipykernel = False self.connection_file = None def get_icon(self): - return get_icon('cmdprompt.png') - + return ima.icon('cmdprompt') + + def finish_process(self): + while not self.process.waitForFinished(100): + self.process.kill(); + def create_process(self): self.shell.clear() - + self.process = QProcess(self) self.process.setProcessChannelMode(QProcess.MergedChannels) - + # PYTHONPATH (in case we use Python in this terminal, e.g. py2exe) env = [to_text_string(_path) for _path in self.process.systemEnvironment()] + + processEnvironment = QProcessEnvironment() + for envItem in env: + envName, separator, envValue = envItem.partition('=') + processEnvironment.insert(envName, envValue) + add_pathlist_to_PYTHONPATH(env, self.path) - self.process.setEnvironment(env) + self.process.setProcessEnvironment(processEnvironment) + # Working directory if self.wdir is not None: @@ -71,14 +87,10 @@ if self.arguments: p_args.extend( shell_split(self.arguments) ) - - self.connect(self.process, SIGNAL("readyReadStandardOutput()"), - self.write_output) - self.connect(self.process, SIGNAL("finished(int,QProcess::ExitStatus)"), - self.finished) - self.connect(self.kill_button, SIGNAL("clicked()"), - self.process.kill) + self.process.readyReadStandardOutput.connect(self.write_output) + self.process.finished.connect(self.finished) + self.kill_button.clicked.connect(self.process.kill) if os.name == 'nt': self.process.start('cmd.exe', p_args) @@ -94,7 +106,7 @@ _("Process failed to start")) else: self.shell.setFocus() - self.emit(SIGNAL('started()')) + self.started.emit() return self.process @@ -143,4 +155,30 @@ # x.dwProcessID) # else: # self.send_ctrl_to_process('c') - \ Kein Zeilenumbruch am Dateiende. + + +#============================================================================== +# Tests +#============================================================================== +def test(): + import os.path as osp + from spyder.utils.qthelpers import qapplication + app = qapplication(test_time=5) + shell = ExternalSystemShell(wdir=osp.dirname(__file__), + light_background=False) + + app.aboutToQuit.connect(shell.finish_process) + + from qtpy.QtGui import QFont + font = QFont() + font.setPointSize(10) + shell.shell.set_font(font) + + shell.shell.toggle_wrap_mode(True) + shell.start_shell(False) + shell.show() + sys.exit(app.exec_()) + + +if __name__ == "__main__": + test() \ Kein Zeilenumbruch am Dateiende. diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/widgets/fileswitcher.py spyder-3.0.2+dfsg1/spyder/widgets/fileswitcher.py --- spyder-2.3.8+dfsg1/spyder/widgets/fileswitcher.py 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/widgets/fileswitcher.py 2016-11-16 17:12:05.000000000 +0100 @@ -0,0 +1,609 @@ +# -*- coding: utf-8 -*- +# +# Copyright © Spyder Project Contributors +# Licensed under the terms of the MIT License +# (see spyder/__init__.py for details) + +# Standard library imports +from __future__ import print_function +import os +import os.path as osp + +# Third party imports +from qtpy.QtCore import Signal, QEvent, QObject, QRegExp, QSize, Qt +from qtpy.QtGui import (QIcon, QRegExpValidator, QTextCursor) +from qtpy.QtWidgets import (QDialog, QHBoxLayout, QLabel, QLineEdit, + QListWidget, QListWidgetItem, QVBoxLayout) + +# Local imports +from spyder.config.base import _ +from spyder.py3compat import iteritems, to_text_string +from spyder.utils import icon_manager as ima +from spyder.utils.stringmatching import get_search_scores +from spyder.widgets.helperwidgets import HelperToolButton, HTMLDelegate + + +# --- Python Outline explorer helpers +def process_python_symbol_data(oedata): + """Returns a list with line number, definition name, fold and token.""" + symbol_list = [] + for key in oedata: + val = oedata[key] + if val and key != 'found_cell_separators': + if val.is_class_or_function(): + symbol_list.append((key, val.def_name, val.fold_level, + val.get_token())) + return sorted(symbol_list) + + +def get_python_symbol_icons(oedata): + """Return a list of icons for oedata of a python file.""" + class_icon = ima.icon('class') + method_icon = ima.icon('method') + function_icon = ima.icon('function') + private_icon = ima.icon('private1') + super_private_icon = ima.icon('private2') + + symbols = process_python_symbol_data(oedata) + + # line - 1, name, fold level + fold_levels = sorted(list(set([s[2] for s in symbols]))) + parents = [None]*len(symbols) + icons = [None]*len(symbols) + indexes = [] + + parent = None + for level in fold_levels: + for index, item in enumerate(symbols): + line, name, fold_level, token = item + if index in indexes: + continue + + if fold_level == level: + indexes.append(index) + parent = item + else: + parents[index] = parent + + for index, item in enumerate(symbols): + parent = parents[index] + + if item[-1] == 'def': + icons[index] = function_icon + elif item[-1] == 'class': + icons[index] = class_icon + else: + icons[index] = QIcon() + + if parent is not None: + if parent[-1] == 'class': + if item[-1] == 'def' and item[1].startswith('__'): + icons[index] = super_private_icon + elif item[-1] == 'def' and item[1].startswith('_'): + icons[index] = private_icon + else: + icons[index] = method_icon + + return icons + + +def shorten_paths(path_list, is_unsaved): + """ + Takes a list of paths and tries to "intelligently" shorten them all. The + aim is to make it clear to the user where the paths differ, as that is + likely what they care about. Note that this operates on a list of paths + not on individual paths. + + If the path ends in an actual file name, it will be trimmed off. + """ + # TODO: at the end, if the path is too long, should do a more dumb kind of + # shortening, but not completely dumb. + + # Convert the path strings to a list of tokens and start building the + # new_path using the drive + path_list = path_list[:] # Make a local copy + new_path_list = [] + + for ii, (path, is_unsav) in enumerate(zip(path_list, is_unsaved)): + if is_unsav: + new_path_list.append(_('unsaved file')) + path_list[ii] = None + else: + drive, path = osp.splitdrive(osp.dirname(path)) + new_path_list.append(drive + osp.sep) + path_list[ii] = [part for part in path.split(osp.sep) if part] + + def recurse_level(level_idx): + sep = os.sep + + # If toks are all empty we need not have recursed here + if not any(level_idx.values()): + return + + # Firstly, find the longest common prefix for all in the level + # s = len of longest common prefix + sample_toks = list(level_idx.values())[0] + if not sample_toks: + s = 0 + else: + for s, sample_val in enumerate(sample_toks): + if not all(len(toks) > s and toks[s] == sample_val + for toks in level_idx.values()): + break + + # Shorten longest common prefix + if s == 0: + short_form = '' + else: + if s == 1: + short_form = sample_toks[0] + elif s == 2: + short_form = sample_toks[0] + sep + sample_toks[1] + else: + short_form = "..." + sep + sample_toks[s-1] + for idx in level_idx: + new_path_list[idx] += short_form + sep + level_idx[idx] = level_idx[idx][s:] + + # Group the remaining bit after the common prefix, shorten, and recurse + while level_idx: + k, group = 0, level_idx # k is length of the group's common prefix + while True: + # Abort if we've gone beyond end of one or more in the group + prospective_group = {idx: toks for idx, toks + in group.items() if len(toks) == k} + if prospective_group: + if k == 0: # we spit out the group with no suffix + group = prospective_group + break + # Only keep going if all n still match on the kth token + _, sample_toks = next(iteritems(group)) + prospective_group = {idx: toks for idx, toks + in group.items() + if toks[k] == sample_toks[k]} + if len(prospective_group) == len(group) or k == 0: + group = prospective_group + k += 1 + else: + break + _, sample_toks = next(iteritems(group)) + if k == 0: + short_form = '' + elif k == 1: + short_form = sample_toks[0] + elif k == 2: + short_form = sample_toks[0] + sep + sample_toks[1] + else: # k > 2 + short_form = sample_toks[0] + "..." + sep + sample_toks[k-1] + for idx in group.keys(): + new_path_list[idx] += short_form + (sep if k > 0 else '') + del level_idx[idx] + recurse_level({idx: toks[k:] for idx, toks in group.items()}) + + recurse_level({i: pl for i, pl in enumerate(path_list) if pl}) + + return [path.rstrip(os.sep) for path in new_path_list] + + +class KeyPressFilter(QObject): + """ + Use with `installEventFilter` to get up/down arrow key press signal. + """ + UP, DOWN = [-1, 1] # Step constants + + sig_up_key_pressed = Signal() + sig_down_key_pressed = Signal() + + def eventFilter(self, src, e): + if e.type() == QEvent.KeyPress: + if e.key() == Qt.Key_Up: + self.sig_up_key_pressed.emit() + elif e.key() == Qt.Key_Down: + self.sig_down_key_pressed.emit() + + return super(KeyPressFilter, self).eventFilter(src, e) + + +class FileSwitcher(QDialog): + """A Sublime-like file switcher.""" + sig_goto_file = Signal(int) + sig_close_file = Signal(int) + + # Constants that define the mode in which the list widget is working + # FILE_MODE is for a list of files, SYMBOL_MODE if for a list of symbols + # in a given file when using the '@' symbol. + FILE_MODE, SYMBOL_MODE = [1, 2] + + def __init__(self, parent, tabs, data): + QDialog.__init__(self, parent) + + # Variables + self.tabs = tabs # Editor stack tabs + self.data = data # Editor data + self.mode = self.FILE_MODE # By default start in this mode + self.initial_cursors = None # {fullpath: QCursor} + self.initial_path = None # Fullpath of initial active editor + self.initial_editor = None # Initial active editor + self.line_number = None # Selected line number in filer + self.is_visible = False # Is the switcher visible? + + help_text = _("Press Enter to switch files or Esc to " + "cancel.

    Type to filter filenames.

    " + "Use :number to go to a line, e.g. " + "main:42
    " + "Use @symbol_text to go to a symbol, e.g. " + "@init" + "

    Press Ctrl+W to close current tab.
    ") + + # Either allow searching for a line number or a symbol but not both + regex = QRegExp("([A-Za-z0-9_]{0,100}@[A-Za-z0-9_]{0,100})|" + + "([A-Za-z0-9_]{0,100}:{0,1}[0-9]{0,100})") + + # Widgets + self.edit = QLineEdit(self) + self.help = HelperToolButton() + self.list = QListWidget(self) + self.filter = KeyPressFilter() + regex_validator = QRegExpValidator(regex, self.edit) + + # Widgets setup + self.setWindowFlags(Qt.Popup | Qt.FramelessWindowHint) + self.setWindowOpacity(0.95) + self.edit.installEventFilter(self.filter) + self.edit.setValidator(regex_validator) + self.help.setToolTip(help_text) + self.list.setItemDelegate(HTMLDelegate(self)) + + # Layout + edit_layout = QHBoxLayout() + edit_layout.addWidget(self.edit) + edit_layout.addWidget(self.help) + layout = QVBoxLayout() + layout.addLayout(edit_layout) + layout.addWidget(self.list) + self.setLayout(layout) + + # Signals + self.rejected.connect(self.restore_initial_state) + self.filter.sig_up_key_pressed.connect(self.previous_row) + self.filter.sig_down_key_pressed.connect(self.next_row) + self.edit.returnPressed.connect(self.accept) + self.edit.textChanged.connect(self.setup) + self.list.itemSelectionChanged.connect(self.item_selection_changed) + self.list.clicked.connect(self.edit.setFocus) + + # Setup + self.save_initial_state() + self.set_dialog_position() + self.setup() + + # --- Properties + @property + def editors(self): + return [self.tabs.widget(index) for index in range(self.tabs.count())] + + @property + def line_count(self): + return [editor.get_line_count() for editor in self.editors] + + @property + def save_status(self): + return [getattr(td, 'newly_created', False) for td in self.data] + + @property + def paths(self): + return [getattr(td, 'filename', None) for td in self.data] + + @property + def filenames(self): + return [self.tabs.tabText(index) for index in range(self.tabs.count())] + + @property + def current_path(self): + return self.paths_by_editor[self.get_editor()] + + @property + def paths_by_editor(self): + return dict(zip(self.editors, self.paths)) + + @property + def editors_by_path(self): + return dict(zip(self.paths, self.editors)) + + @property + def filter_text(self): + """Get the normalized (lowecase) content of the filter text.""" + return to_text_string(self.edit.text()).lower() + + def save_initial_state(self): + """Saves initial cursors and initial active editor.""" + paths = self.paths + self.initial_editor = self.get_editor() + self.initial_cursors = {} + + for i, editor in enumerate(self.editors): + if editor is self.initial_editor: + self.initial_path = paths[i] + self.initial_cursors[paths[i]] = editor.textCursor() + + def accept(self): + self.is_visible = False + QDialog.accept(self) + self.list.clear() + + def restore_initial_state(self): + """Restores initial cursors and initial active editor.""" + self.list.clear() + self.is_visible = False + editors = self.editors_by_path + + for path in self.initial_cursors: + cursor = self.initial_cursors[path] + if path in editors: + self.set_editor_cursor(editors[path], cursor) + + if self.initial_editor in self.paths_by_editor: + index = self.paths.index(self.initial_path) + self.sig_goto_file.emit(index) + + def set_dialog_position(self): + """Positions the file switcher dialog in the center of the editor.""" + parent = self.parent() + geo = parent.geometry() + width = self.list.width() # This has been set in setup + + left = parent.geometry().width()/2 - width/2 + top = 0 + while parent: + geo = parent.geometry() + top += geo.top() + left += geo.left() + parent = parent.parent() + + # Note: the +1 pixel on the top makes it look better + self.move(left, top + self.tabs.tabBar().geometry().height() + 1) + + def fix_size(self, content, extra=50): + """ + Adjusts the width and height of the file switcher, + based on its content. + """ + # Update size of dialog based on longest shortened path + strings = [] + if content: + for rich_text in content: + label = QLabel(rich_text) + label.setTextFormat(Qt.PlainText) + strings.append(label.text()) + fm = label.fontMetrics() + + # Max width + max_width = max([fm.width(s) * 1.3 for s in strings]) + self.list.setMinimumWidth(max_width + extra) + + # Max height + if len(strings) < 8: + max_entries = len(strings) + else: + max_entries = 8 + max_height = fm.height() * max_entries * 2.5 + self.list.setMinimumHeight(max_height) + + # Set position according to size + self.set_dialog_position() + + # --- Helper methods: List widget + def count(self): + """Gets the item count in the list widget.""" + return self.list.count() + + def current_row(self): + """Returns the current selected row in the list widget.""" + return self.list.currentRow() + + def set_current_row(self, row): + """Sets the current selected row in the list widget.""" + return self.list.setCurrentRow(row) + + def select_row(self, steps): + """Select row in list widget based on a number of steps with direction. + + Steps can be positive (next rows) or negative (previous rows). + """ + row = self.current_row() + steps + if 0 <= row < self.count(): + self.set_current_row(row) + + def previous_row(self): + """Select previous row in list widget.""" + self.select_row(-1) + + def next_row(self): + """Select next row in list widget.""" + self.select_row(+1) + + # --- Helper methods: Editor + def get_editor(self, index=None, path=None): + """Get editor by index or path. + + If no path or index specified the current active editor is returned + """ + if index: + return self.tabs.widget(index) + elif path: + return self.tabs.widget(index) + else: + return self.parent().get_current_editor() + + def set_editor_cursor(self, editor, cursor): + """Set the cursor of an editor.""" + pos = cursor.position() + anchor = cursor.anchor() + + new_cursor = QTextCursor() + if pos == anchor: + new_cursor.movePosition(pos) + else: + new_cursor.movePosition(anchor) + new_cursor.movePosition(pos, QTextCursor.KeepAnchor) + editor.setTextCursor(cursor) + + def goto_line(self, line_number): + """Go to specified line number in current active editor.""" + if line_number: + line_number = int(line_number) + editor = self.get_editor() + editor.go_to_line(min(line_number, editor.get_line_count())) + + # --- Helper methods: Outline explorer + def get_symbol_list(self): + """Get the list of symbols present in the file.""" + try: + oedata = self.get_editor().get_outlineexplorer_data() + except AttributeError: + oedata = {} + return oedata + + # --- Handlers + def item_selection_changed(self): + """List widget item selection change handler.""" + row = self.current_row() + if self.count() and row >= 0: + if self.mode == self.FILE_MODE: + try: + stack_index = self.paths.index(self.filtered_path[row]) + self.sig_goto_file.emit(stack_index) + self.goto_line(self.line_number) + self.edit.setFocus() + except ValueError: + pass + else: + line_number = self.filtered_symbol_lines[row] + self.goto_line(line_number) + + def setup_file_list(self, filter_text, current_path): + """Setup list widget content for file list display.""" + short_paths = shorten_paths(self.paths, self.save_status) + paths = self.paths + results = [] + trying_for_line_number = ':' in filter_text + + # Get optional line number + if trying_for_line_number: + filter_text, line_number = filter_text.split(':') + else: + line_number = None + + # Get all available filenames and get the scores for "fuzzy" matching + scores = get_search_scores(filter_text, self.filenames, + template="{0}") + + # Build the text that will appear on the list widget + for index, score in enumerate(scores): + text, rich_text, score_value = score + if score_value != -1: + text_item = '' + rich_text.replace('&', '') + '' + if trying_for_line_number: + text_item += " [{0:} {1:}]".format(self.line_count[index], + _("lines")) + text_item += "
    {0:}".format(short_paths[index]) + results.append((score_value, index, text_item)) + + # Sort the obtained scores and populate the list widget + self.filtered_path = [] + for result in sorted(results): + index = result[1] + text = result[-1] + path = paths[index] + item = QListWidgetItem(ima.icon('FileIcon'), text) + item.setToolTip(path) + item.setSizeHint(QSize(0, 25)) + self.list.addItem(item) + self.filtered_path.append(path) + + # To adjust the delegate layout for KDE themes + self.list.files_list = True + + # Move selected item in list accordingly and update list size + if current_path in self.filtered_path: + self.set_current_row(self.filtered_path.index(current_path)) + elif self.filtered_path: + self.set_current_row(0) + self.fix_size(short_paths, extra=200) + + # If a line number is searched look for it + self.line_number = line_number + self.goto_line(line_number) + + def setup_symbol_list(self, filter_text, current_path): + """Setup list widget content for symbol list display.""" + # Get optional symbol name + filter_text, symbol_text = filter_text.split('@') + + # Fetch the Outline explorer data, get the icons and values + oedata = self.get_symbol_list() + icons = get_python_symbol_icons(oedata) + + symbol_list = process_python_symbol_data(oedata) + line_fold_token = [(item[0], item[2], item[3]) for item in symbol_list] + choices = [item[1] for item in symbol_list] + scores = get_search_scores(symbol_text, choices, template="{0}") + + # Build the text that will appear on the list widget + results = [] + lines = [] + self.filtered_symbol_lines = [] + for index, score in enumerate(scores): + text, rich_text, score_value = score + line, fold_level, token = line_fold_token[index] + lines.append(text) + if score_value != -1: + results.append((score_value, line, text, rich_text, + fold_level, icons[index], token)) + + template = '{0}{1}' + + for (score, line, text, rich_text, fold_level, icon, + token) in sorted(results): + fold_space = ' '*(fold_level) + line_number = line + 1 + self.filtered_symbol_lines.append(line_number) + textline = template.format(fold_space, rich_text) + item = QListWidgetItem(icon, textline) + item.setSizeHint(QSize(0, 16)) + self.list.addItem(item) + + # To adjust the delegate layout for KDE themes + self.list.files_list = False + + # Move selected item in list accordingly + # NOTE: Doing this is causing two problems: + # 1. It makes the cursor to auto-jump to the last selected + # symbol after opening or closing a different file + # 2. It moves the cursor to the first symbol by default, + # which is very distracting. + # That's why this line is commented! + # self.set_current_row(0) + + # Update list size + self.fix_size(lines, extra=125) + + def setup(self): + """Setup list widget content.""" + if not self.tabs.count(): + self.close() + return + + self.list.clear() + current_path = self.current_path + filter_text = self.filter_text + + # Get optional line or symbol to define mode and method handler + trying_for_symbol = ('@' in self.filter_text) + + if trying_for_symbol: + self.mode = self.SYMBOL_MODE + self.setup_symbol_list(filter_text, current_path) + else: + self.mode = self.FILE_MODE + self.setup_file_list(filter_text, current_path) diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/widgets/findinfiles.py spyder-3.0.2+dfsg1/spyder/widgets/findinfiles.py --- spyder-2.3.8+dfsg1/spyder/widgets/findinfiles.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/widgets/findinfiles.py 2016-11-16 03:30:06.000000000 +0100 @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- # -# Copyright © 2009-2010 Pierre Raybaut +# Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) +# (see spyder/__init__.py for details) """Find in files widget""" @@ -11,30 +11,31 @@ # pylint: disable=R0911 # pylint: disable=R0201 +# Standard library imports from __future__ import with_statement - -from spyderlib.qt.QtGui import (QHBoxLayout, QWidget, QTreeWidgetItem, - QSizePolicy, QRadioButton, QVBoxLayout, QLabel) -from spyderlib.qt.QtCore import SIGNAL, Qt, QThread, QMutexLocker, QMutex -from spyderlib.qt.compat import getexistingdirectory - -import sys -import os -import re import fnmatch +import os import os.path as osp -from subprocess import Popen, PIPE +import re +import sys import traceback +# Third party imports +from qtpy.compat import getexistingdirectory +from qtpy.QtCore import QMutex, QMutexLocker, Qt, QThread, Signal, Slot +from qtpy.QtWidgets import (QHBoxLayout, QLabel, QRadioButton, QSizePolicy, + QTreeWidgetItem, QVBoxLayout, QWidget) + # Local imports -from spyderlib.utils.vcs import is_hg_installed, get_vcs_root -from spyderlib.utils.misc import abspardir, get_common_path -from spyderlib.utils.qthelpers import (get_icon, get_std_icon, - create_toolbutton, get_filetype_icon) -from spyderlib.baseconfig import _ -from spyderlib.widgets.comboboxes import PathComboBox, PatternComboBox -from spyderlib.widgets.onecolumntree import OneColumnTree -from spyderlib.py3compat import to_text_string, getcwd +from spyder.config.base import _ +from spyder.py3compat import getcwd, to_text_string +from spyder.utils import programs +from spyder.utils import icon_manager as ima +from spyder.utils.misc import abspardir, get_common_path +from spyder.utils.qthelpers import create_toolbutton, get_filetype_icon +from spyder.utils.vcs import is_hg_installed, get_vcs_root +from spyder.widgets.comboboxes import PathComboBox, PatternComboBox +from spyder.widgets.onecolumntree import OneColumnTree #def find_files_in_hg_manifest(rootpath, include, exclude): @@ -106,6 +107,8 @@ class SearchThread(QThread): """Find in files search thread""" + sig_finished = Signal(bool) + def __init__(self, parent): QThread.__init__(self, parent) self.mutex = QMutex() @@ -154,7 +157,7 @@ traceback.print_exc() self.error_flag = _("Unexpected error: see internal console") self.stop() - self.emit(SIGNAL("finished(bool)"), self.completed) + self.sig_finished.emit(self.completed) def stop(self): with QMutexLocker(self.mutex): @@ -186,8 +189,7 @@ return ok def find_files_in_hg_manifest(self): - p = Popen(['hg', 'manifest'], stdout=PIPE, - cwd=self.rootpath, shell=True) + p = programs.run_shell_command('hg manifest', cwd=self.rootpath) hgroot = get_vcs_root(self.rootpath) self.pathlist = [hgroot] for path in p.stdout.read().decode().splitlines(): @@ -284,6 +286,9 @@ class FindOptions(QWidget): """Find widget with options""" + find = Signal() + stop = Signal() + def __init__(self, parent, search_text, search_text_regexp, search_path, include, include_idx, include_regexp, exclude, exclude_idx, exclude_regexp, @@ -309,8 +314,8 @@ self.search_text = PatternComboBox(self, search_text, _("Search pattern")) self.edit_regexp = create_toolbutton(self, - icon=get_icon("advanced.png"), - tip=_("Regular expression")) + icon=ima.icon('advanced'), + tip=_('Regular expression')) self.edit_regexp.setCheckable(True) self.edit_regexp.setChecked(search_text_regexp) self.more_widgets = () @@ -320,14 +325,14 @@ self.more_options.setChecked(more_options) self.ok_button = create_toolbutton(self, text=_("Search"), - icon=get_std_icon("DialogApplyButton"), - triggered=lambda: self.emit(SIGNAL('find()')), + icon=ima.icon('DialogApplyButton'), + triggered=lambda: self.find.emit(), tip=_("Start search"), text_beside_icon=True) - self.connect(self.ok_button, SIGNAL('clicked()'), self.update_combos) + self.ok_button.clicked.connect(self.update_combos) self.stop_button = create_toolbutton(self, text=_("Stop"), - icon=get_icon("stop.png"), - triggered=lambda: self.emit(SIGNAL('stop()')), + icon=ima.icon('stop'), + triggered=lambda: self.stop.emit(), tip=_("Stop search"), text_beside_icon=True) self.stop_button.setEnabled(False) @@ -343,8 +348,8 @@ and include_idx < self.include_pattern.count(): self.include_pattern.setCurrentIndex(include_idx) self.include_regexp = create_toolbutton(self, - icon=get_icon("advanced.png"), - tip=_("Regular expression")) + icon=ima.icon('advanced'), + tip=_('Regular expression')) self.include_regexp.setCheckable(True) self.include_regexp.setChecked(include_regexp) include_label = QLabel(_("Include:")) @@ -355,8 +360,8 @@ and exclude_idx < self.exclude_pattern.count(): self.exclude_pattern.setCurrentIndex(exclude_idx) self.exclude_regexp = create_toolbutton(self, - icon=get_icon("advanced.png"), - tip=_("Regular expression")) + icon=ima.icon('advanced'), + tip=_('Regular expression')) self.exclude_regexp.setCheckable(True) self.exclude_regexp.setChecked(exclude_regexp) exclude_label = QLabel(_("Exclude:")) @@ -383,27 +388,20 @@ self.dir_combo = PathComboBox(self) self.dir_combo.addItems(search_path) self.dir_combo.setToolTip(_("Search recursively in this directory")) - self.connect(self.dir_combo, SIGNAL("open_dir(QString)"), - self.set_directory) - self.connect(self.python_path, SIGNAL('toggled(bool)'), - self.dir_combo.setDisabled) - self.connect(self.hg_manifest, SIGNAL('toggled(bool)'), - self.dir_combo.setDisabled) - browse = create_toolbutton(self, icon=get_std_icon('DirOpenIcon'), + self.dir_combo.open_dir.connect(self.set_directory) + self.python_path.toggled.connect(self.dir_combo.setDisabled) + self.hg_manifest.toggled.connect(self.dir_combo.setDisabled) + browse = create_toolbutton(self, icon=ima.icon('DirOpenIcon'), tip=_('Browse a search directory'), triggered=self.select_directory) for widget in [self.python_path, self.hg_manifest, self.custom_dir, self.dir_combo, browse]: hlayout3.addWidget(widget) - self.connect(self.search_text, SIGNAL("valid(bool)"), - lambda valid: self.emit(SIGNAL('find()'))) - self.connect(self.include_pattern, SIGNAL("valid(bool)"), - lambda valid: self.emit(SIGNAL('find()'))) - self.connect(self.exclude_pattern, SIGNAL("valid(bool)"), - lambda valid: self.emit(SIGNAL('find()'))) - self.connect(self.dir_combo, SIGNAL("valid(bool)"), - lambda valid: self.emit(SIGNAL('find()'))) + 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) @@ -415,25 +413,26 @@ self.setLayout(vlayout) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum) - + + @Slot(bool) def toggle_more_options(self, state): for layout in self.more_widgets: for index in range(layout.count()): if state and self.isVisible() or not state: layout.itemAt(index).widget().setVisible(state) if state: - icon_name = 'options_less.png' + icon = ima.icon('options_less') tip = _('Hide advanced options') else: - icon_name = 'options_more.png' + icon = ima.icon('options_more') tip = _('Show advanced options') - self.more_options.setIcon(get_icon(icon_name)) + self.more_options.setIcon(icon) self.more_options.setToolTip(tip) def update_combos(self): - self.search_text.lineEdit().emit(SIGNAL('returnPressed()')) - self.include_pattern.lineEdit().emit(SIGNAL('returnPressed()')) - self.exclude_pattern.lineEdit().emit(SIGNAL('returnPressed()')) + self.search_text.lineEdit().returnPressed.emit() + self.include_pattern.lineEdit().returnPressed.emit() + self.exclude_pattern.lineEdit().returnPressed.emit() def detect_hg_repository(self, path=None): if path is None: @@ -472,7 +471,7 @@ hg_manifest = self.hg_manifest.isChecked() path = osp.abspath( to_text_string( self.dir_combo.currentText() ) ) - # Finding text occurences + # Finding text occurrences if not include_re: include = fnmatch.translate(include) if not exclude_re: @@ -497,15 +496,16 @@ else: return (path, python_path, hg_manifest, include, exclude, texts, text_re) - + + @Slot() def select_directory(self): """Select directory""" - self.parent().emit(SIGNAL('redirect_stdio(bool)'), False) + self.parent().redirect_stdio.emit(False) directory = getexistingdirectory(self, _("Select directory"), self.dir_combo.currentText()) if directory: self.set_directory(directory) - self.parent().emit(SIGNAL('redirect_stdio(bool)'), True) + self.parent().redirect_stdio.emit(True) def set_directory(self, directory): path = to_text_string(osp.abspath(to_text_string(directory))) @@ -517,11 +517,10 @@ ctrl = event.modifiers() & Qt.ControlModifier shift = event.modifiers() & Qt.ShiftModifier if event.key() in (Qt.Key_Enter, Qt.Key_Return): - self.emit(SIGNAL('find()')) + self.find.emit() elif event.key() == Qt.Key_F and ctrl and shift: # Toggle find widgets - self.parent().emit(SIGNAL('toggle_visibility(bool)'), - not self.isVisible()) + self.parent().toggle_visibility.emit(not self.isVisible()) else: QWidget.keyPressEvent(self, event) @@ -543,8 +542,7 @@ itemdata = self.data.get(id(self.currentItem())) if itemdata is not None: filename, lineno = itemdata - self.parent().emit(SIGNAL("edit_goto(QString,int,QString)"), - filename, lineno, self.search_text) + self.parent().edit_goto.emit(filename, lineno, self.search_text) def clicked(self, item): """Click event""" @@ -620,7 +618,7 @@ displayed_name = dirname item = QTreeWidgetItem(parent, [displayed_name], QTreeWidgetItem.Type) - item.setIcon(0, get_std_icon('DirClosedIcon')) + item.setIcon(0, ima.icon('DirClosedIcon')) return item dirs = {} for dirname in sorted(list(dir_set)): @@ -664,7 +662,7 @@ item = QTreeWidgetItem(file_item, ["%d (%s): %s" % (lineno, colno_str, line.rstrip())], QTreeWidgetItem.Type) - item.setIcon(0, get_icon('arrow.png')) + item.setIcon(0, ima.icon('arrow')) self.data[id(item)] = (filename, lineno) # Removing empty directories top_level_items = [self.topLevelItem(index) @@ -699,9 +697,8 @@ exclude, exclude_idx, exclude_regexp, supported_encodings, in_python_path, more_options) - self.connect(self.find_options, SIGNAL('find()'), self.find) - self.connect(self.find_options, SIGNAL('stop()'), - self.stop_and_reset_thread) + self.find_options.find.connect(self.find) + self.find_options.stop.connect(self.stop_and_reset_thread) self.result_browser = ResultsBrowser(self) @@ -748,8 +745,7 @@ self.search_thread = SearchThread(self) self.search_thread.get_pythonpath_callback = \ self.get_pythonpath_callback - self.connect(self.search_thread, SIGNAL("finished(bool)"), - self.search_complete) + self.search_thread.sig_finished.connect(self.search_complete) self.search_thread.initialize(*options) self.search_thread.start() self.find_options.ok_button.setEnabled(False) @@ -760,9 +756,8 @@ if self.search_thread is not None: if self.search_thread.isRunning(): if ignore_results: - self.disconnect(self.search_thread, - SIGNAL("finished(bool)"), - self.search_complete) + self.search_thread.sig_finished.disconnect( + self.search_complete) self.search_thread.stop() self.search_thread.wait() self.search_thread.setParent(None) @@ -791,12 +786,13 @@ def test(): """Run Find in Files widget test""" - from spyderlib.utils.qthelpers import qapplication + from spyder.utils.qthelpers import qapplication app = qapplication() widget = FindInFilesWidget(None) + widget.resize(640, 480) widget.show() sys.exit(app.exec_()) - + + if __name__ == '__main__': test() - \ Kein Zeilenumbruch am Dateiende. diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/widgets/findreplace.py spyder-3.0.2+dfsg1/spyder/widgets/findreplace.py --- spyder-2.3.8+dfsg1/spyder/widgets/findreplace.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/widgets/findreplace.py 2016-10-25 02:05:23.000000000 +0200 @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- # -# Copyright © 2009-2010 Pierre Raybaut +# Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) +# (see spyder/__init__.py for details) """Find/Replace widget""" @@ -11,19 +11,22 @@ # pylint: disable=R0911 # pylint: disable=R0201 -from spyderlib.qt.QtGui import (QHBoxLayout, QGridLayout, QCheckBox, QLabel, - QWidget, QSizePolicy, QTextCursor) -from spyderlib.qt.QtCore import SIGNAL, Qt, QTimer - +# Standard library imports import re +# Third party imports +from qtpy.QtCore import Qt, QTimer, Signal, Slot +from qtpy.QtGui import QTextCursor +from qtpy.QtWidgets import (QCheckBox, QGridLayout, QHBoxLayout, QLabel, + QSizePolicy, QWidget) + # Local imports -from spyderlib.baseconfig import _ -from spyderlib.guiconfig import create_shortcut, new_shortcut -from spyderlib.utils.qthelpers import (get_icon, get_std_icon, - create_toolbutton) -from spyderlib.widgets.comboboxes import PatternComboBox -from spyderlib.py3compat import to_text_string +from spyder.config.base import _ +from spyder.config.gui import config_shortcut, fixed_shortcut +from spyder.py3compat import to_text_string +from spyder.utils import icon_manager as ima +from spyder.utils.qthelpers import create_toolbutton, get_icon +from spyder.widgets.comboboxes import PatternComboBox def is_position_sup(pos1, pos2): @@ -36,14 +39,12 @@ class FindReplace(QWidget): - """ - Find widget - - Signals: - visibility_changed(bool) - """ + """Find widget""" STYLE = {False: "background-color:rgb(255, 175, 90);", - True: ""} + True: "", + None: ""} + visibility_changed = Signal(bool) + def __init__(self, parent, enable_replace=False): QWidget.__init__(self, parent) self.enable_replace = enable_replace @@ -55,55 +56,49 @@ self.setLayout(glayout) self.close_button = create_toolbutton(self, triggered=self.hide, - icon=get_std_icon("DialogCloseButton")) + icon=ima.icon('DialogCloseButton')) glayout.addWidget(self.close_button, 0, 0) # Find layout self.search_text = PatternComboBox(self, tip=_("Search string"), adjust_to_minimum=False) - self.connect(self.search_text, SIGNAL('valid(bool)'), + self.search_text.valid.connect( lambda state: self.find(changed=False, forward=True, rehighlight=False)) - self.connect(self.search_text.lineEdit(), - SIGNAL("textEdited(QString)"), self.text_has_been_edited) + self.search_text.lineEdit().textEdited.connect( + self.text_has_been_edited) self.previous_button = create_toolbutton(self, triggered=self.find_previous, - icon=get_std_icon("ArrowBack")) + icon=ima.icon('ArrowUp')) self.next_button = create_toolbutton(self, triggered=self.find_next, - icon=get_std_icon("ArrowForward")) - self.connect(self.next_button, SIGNAL('clicked()'), - self.update_search_combo) - self.connect(self.previous_button, SIGNAL('clicked()'), - self.update_search_combo) + icon=ima.icon('ArrowDown')) + self.next_button.clicked.connect(self.update_search_combo) + self.previous_button.clicked.connect(self.update_search_combo) - self.re_button = create_toolbutton(self, icon=get_icon("advanced.png"), + self.re_button = create_toolbutton(self, icon=ima.icon('advanced'), tip=_("Regular expression")) self.re_button.setCheckable(True) - self.connect(self.re_button, SIGNAL("toggled(bool)"), - lambda state: self.find()) + self.re_button.toggled.connect(lambda state: self.find()) self.case_button = create_toolbutton(self, icon=get_icon("upper_lower.png"), tip=_("Case Sensitive")) self.case_button.setCheckable(True) - self.connect(self.case_button, SIGNAL("toggled(bool)"), - lambda state: self.find()) + self.case_button.toggled.connect(lambda state: self.find()) self.words_button = create_toolbutton(self, icon=get_icon("whole_words.png"), tip=_("Whole words")) self.words_button.setCheckable(True) - self.connect(self.words_button, SIGNAL("toggled(bool)"), - lambda state: self.find()) + self.words_button.toggled.connect(lambda state: self.find()) self.highlight_button = create_toolbutton(self, icon=get_icon("highlight.png"), tip=_("Highlight matches")) self.highlight_button.setCheckable(True) - self.connect(self.highlight_button, SIGNAL("toggled(bool)"), - self.toggle_highlighting) + self.highlight_button.toggled.connect(self.toggle_highlighting) hlayout = QHBoxLayout() self.widgets = [self.close_button, self.search_text, @@ -117,17 +112,15 @@ # Replace layout replace_with = QLabel(_("Replace with:")) self.replace_text = PatternComboBox(self, adjust_to_minimum=False, - tip=_("Replace string")) + tip=_('Replace string')) self.replace_button = create_toolbutton(self, - text=_("Replace/find"), - icon=get_std_icon("DialogApplyButton"), + text=_('Replace/find'), + icon=ima.icon('DialogApplyButton'), triggered=self.replace_find, text_beside_icon=True) - self.connect(self.replace_button, SIGNAL('clicked()'), - self.update_replace_combo) - self.connect(self.replace_button, SIGNAL('clicked()'), - self.update_search_combo) + self.replace_button.clicked.connect(self.update_replace_combo) + self.replace_button.clicked.connect(self.update_search_combo) self.all_check = QCheckBox(_("Replace all")) @@ -150,26 +143,25 @@ self.highlight_timer = QTimer(self) self.highlight_timer.setSingleShot(True) self.highlight_timer.setInterval(1000) - self.connect(self.highlight_timer, SIGNAL("timeout()"), - self.highlight_matches) + self.highlight_timer.timeout.connect(self.highlight_matches) def create_shortcuts(self, parent): """Create shortcuts for this widget""" # Configurable - findnext = create_shortcut(self.find_next, context='Editor', + findnext = config_shortcut(self.find_next, context='_', name='Find next', parent=parent) - findprev = create_shortcut(self.find_previous, context='Editor', + findprev = config_shortcut(self.find_previous, context='_', name='Find previous', parent=parent) - togglefind = create_shortcut(self.show, context='Editor', + togglefind = config_shortcut(self.show, context='_', name='Find text', parent=parent) - togglereplace = create_shortcut(self.toggle_replace_widgets, - context='Editor', name='Replace text', + togglereplace = config_shortcut(self.toggle_replace_widgets, + context='_', name='Replace text', parent=parent) # Fixed - new_shortcut("Escape", self, self.hide) + fixed_shortcut("Escape", self, self.hide) return [findnext, findprev, togglefind, togglereplace] - + def get_shortcut_data(self): """ Returns shortcut data, a list of tuples (shortcut, text, default) @@ -180,10 +172,10 @@ return [sc.data for sc in self.shortcuts] def update_search_combo(self): - self.search_text.lineEdit().emit(SIGNAL('returnPressed()')) + self.search_text.lineEdit().returnPressed.emit() def update_replace_combo(self): - self.replace_text.lineEdit().emit(SIGNAL('returnPressed()')) + self.replace_text.lineEdit().returnPressed.emit() def toggle_replace_widgets(self): if self.enable_replace: @@ -194,7 +186,8 @@ else: self.show_replace() self.replace_text.setFocus() - + + @Slot(bool) def toggle_highlighting(self, state): """Toggle the 'highlight all results' feature""" if self.editor is not None: @@ -206,23 +199,37 @@ def show(self): """Overrides Qt Method""" QWidget.show(self) - self.emit(SIGNAL("visibility_changed(bool)"), True) + self.visibility_changed.emit(True) if self.editor is not None: text = self.editor.get_selected_text() - if len(text) > 0: + + # If no text is highlighted for search, use whatever word is under + # the cursor + if not text: + try: + cursor = self.editor.textCursor() + cursor.select(QTextCursor.WordUnderCursor) + text = to_text_string(cursor.selectedText()) + except AttributeError: + # We can't do this for all widgets, e.g. WebView's + pass + + # Now that text value is sorted out, use it for the search + if text: self.search_text.setEditText(text) self.search_text.lineEdit().selectAll() self.refresh() else: self.search_text.lineEdit().selectAll() self.search_text.setFocus() - + + @Slot() def hide(self): """Overrides Qt Method""" for widget in self.replace_widgets: widget.hide() QWidget.hide(self) - self.emit(SIGNAL("visibility_changed(bool)"), False) + self.visibility_changed.emit(False) if self.editor is not None: self.editor.setFocus() self.clear_matches() @@ -257,26 +264,33 @@ browser.WebView """ self.editor = editor - from spyderlib.qt.QtWebKit import QWebView - self.words_button.setVisible(not isinstance(editor, QWebView)) - self.re_button.setVisible(not isinstance(editor, QWebView)) - from spyderlib.widgets.sourcecode.codeeditor import CodeEditor + # Note: This is necessary to test widgets/editor.py + # in Qt builds that don't have web widgets + try: + from qtpy.QtWebEngineWidgets import QWebEngineView + except ImportError: + QWebEngineView = type(None) + self.words_button.setVisible(not isinstance(editor, QWebEngineView)) + self.re_button.setVisible(not isinstance(editor, QWebEngineView)) + from spyder.widgets.sourcecode.codeeditor import CodeEditor self.is_code_editor = isinstance(editor, CodeEditor) self.highlight_button.setVisible(self.is_code_editor) if refresh: self.refresh() if self.isHidden() and editor is not None: self.clear_matches() - + + @Slot() def find_next(self): - """Find next occurence""" + """Find next occurrence""" state = self.find(changed=False, forward=True, rehighlight=False) self.editor.setFocus() self.search_text.add_current_text() return state - + + @Slot() def find_previous(self): - """Find previous occurence""" + """Find previous occurrence""" state = self.find(changed=False, forward=False, rehighlight=False) self.editor.setFocus() return state @@ -306,6 +320,9 @@ text = self.search_text.currentText() if len(text) == 0: self.search_text.lineEdit().setStyleSheet("") + if not self.is_code_editor: + # Clears the selection for WebEngine + self.editor.find_text('') return None else: case = self.case_button.isChecked() @@ -324,7 +341,8 @@ else: self.clear_matches() return found - + + @Slot() def replace_find(self): """Replace and find""" if (self.editor is not None): @@ -368,7 +386,7 @@ # part of the search string break if position1 == position0: - # Avoid infinite loop: single found occurence + # Avoid infinite loop: single found occurrence break position0 = position1 if pattern is None: diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/widgets/formlayout.py spyder-3.0.2+dfsg1/spyder/widgets/formlayout.py --- spyder-2.3.8+dfsg1/spyder/widgets/formlayout.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/widgets/formlayout.py 2016-10-25 02:05:23.000000000 +0200 @@ -33,8 +33,6 @@ OTHER DEALINGS IN THE SOFTWARE. """ -from __future__ import print_function - # History: # 1.0.15: added support for multiline strings # 1.0.14: fixed Python 3 support (regression in 1.0.13) @@ -46,31 +44,34 @@ # 1.0.7: added support for "Apply" button # 1.0.6: code cleaning -__version__ = '1.0.15' -__license__ = __doc__ - +# Standard library imports +from __future__ import print_function +import datetime import os +# Third party imports try: - from spyderlib.qt.QtGui import QFormLayout + from qtpy.QtWidgets import QFormLayout except ImportError: raise ImportError("Warning: formlayout requires PyQt4 >v4.3") -from spyderlib.qt.QtGui import (QWidget, QLineEdit, QComboBox, QLabel, - QSpinBox, QIcon, QStyle, QDialogButtonBox, - QHBoxLayout, QVBoxLayout, QDialog, QColor, - QPushButton, QCheckBox, QColorDialog, QPixmap, - QTabWidget, QApplication, QStackedWidget, - QDateEdit, QDateTimeEdit, QFont, QFontComboBox, - QFontDatabase, QGridLayout, QDoubleValidator, - QTextEdit) -from spyderlib.qt.QtCore import Qt, SIGNAL, SLOT, QSize, Slot, Property -import datetime +from qtpy.QtCore import Property, QSize, Qt, Signal, Slot +from qtpy.QtGui import (QColor, QDoubleValidator, QFont, QFontDatabase, QIcon, + QPixmap) +from qtpy.QtWidgets import (QApplication, QCheckBox, QColorDialog, QComboBox, + QDateEdit, QDateTimeEdit, QDialog, + QDialogButtonBox, QFontComboBox, QGridLayout, + QHBoxLayout, QLabel, QLineEdit, QPushButton, + QSpinBox, QStackedWidget, QStyle, QTabWidget, + QTextEdit, QVBoxLayout, QWidget) # Local imports -from spyderlib.baseconfig import _, DEBUG, STDERR -from spyderlib.py3compat import is_text_string, to_text_string, is_string, u +from spyder.config.base import _, DEBUG, STDERR +from spyder.py3compat import is_string, is_text_string, to_text_string + +__version__ = '1.0.15' +__license__ = __doc__ DEBUG_FORMLAYOUT = DEBUG >= 2 @@ -84,7 +85,7 @@ QPushButton.__init__(self, parent) self.setFixedSize(20, 20) self.setIconSize(QSize(12, 12)) - self.connect(self, SIGNAL("clicked()"), self.choose_color) + self.clicked.connect(self.choose_color) self._color = QColor() def choose_color(self): @@ -99,7 +100,7 @@ def set_color(self, color): if color != self._color: self._color = color - self.emit(SIGNAL("colorChanged(QColor)"), self._color) + self.colorChanged.emit(self._color) pixmap = QPixmap(self.iconSize()) pixmap.fill(color) self.setIcon(QIcon(pixmap)) @@ -134,13 +135,11 @@ QHBoxLayout.__init__(self) assert isinstance(color, QColor) self.lineedit = QLineEdit(color.name(), parent) - self.connect(self.lineedit, SIGNAL("textChanged(QString)"), - self.update_color) + self.lineedit.textChanged.connect(self.update_color) self.addWidget(self.lineedit) self.colorbtn = ColorButton(parent) self.colorbtn.color = color - self.connect(self.colorbtn, SIGNAL("colorChanged(QColor)"), - self.update_text) + self.colorbtn.colorChanged.connect(self.update_text) self.addWidget(self.colorbtn) def update_color(self, text): @@ -231,6 +230,8 @@ return state == QDoubleValidator.Acceptable class FormWidget(QWidget): + update_buttons = Signal() + def __init__(self, data, comment="", parent=None): QWidget.__init__(self, parent) from copy import deepcopy @@ -276,7 +277,7 @@ if '\n' in value: for linesep in (os.linesep, '\n'): if linesep in value: - value = value.replace(linesep, u("\u2029")) + value = value.replace(linesep, u"\u2029") field = QTextEdit(value, self) else: field = QLineEdit(value, self) @@ -308,8 +309,7 @@ field.setValidator(QDoubleValidator(field)) dialog = self.get_dialog() dialog.register_float_field(field) - self.connect(field, SIGNAL('textChanged(QString)'), - lambda text: dialog.update_buttons()) + field.textChanged.connect(lambda text: dialog.update_buttons()) elif isinstance(value, int): field = QSpinBox(self) field.setRange(-1e9, 1e9) @@ -337,7 +337,7 @@ elif is_text_string(value): if isinstance(field, QTextEdit): value = to_text_string(field.toPlainText() - ).replace(u("\u2029"), os.linesep) + ).replace(u"\u2029", os.linesep) else: value = to_text_string(field.text()) elif isinstance(value, (list, tuple)): @@ -374,6 +374,8 @@ class FormComboWidget(QWidget): + update_buttons = Signal() + def __init__(self, datalist, comment="", parent=None): QWidget.__init__(self, parent) layout = QVBoxLayout() @@ -383,8 +385,8 @@ self.stackwidget = QStackedWidget(self) layout.addWidget(self.stackwidget) - self.connect(self.combobox, SIGNAL("currentIndexChanged(int)"), - self.stackwidget, SLOT("setCurrentIndex(int)")) + self.combobox.currentIndexChanged.connect( + self.stackwidget.setCurrentIndex) self.widgetlist = [] for data, title, comment in datalist: @@ -399,9 +401,11 @@ def get(self): return [ widget.get() for widget in self.widgetlist] - + class FormTabWidget(QWidget): + update_buttons = Signal() + def __init__(self, datalist, comment="", parent=None): QWidget.__init__(self, parent) layout = QVBoxLayout() @@ -453,13 +457,12 @@ # Button box self.bbox = bbox = QDialogButtonBox(QDialogButtonBox.Ok |QDialogButtonBox.Cancel) - self.connect(self.formwidget, SIGNAL('update_buttons()'), - self.update_buttons) + self.formwidget.update_buttons.connect(self.update_buttons) if self.apply_callback is not None: apply_btn = bbox.addButton(QDialogButtonBox.Apply) - self.connect(apply_btn, SIGNAL("clicked()"), self.apply) - self.connect(bbox, SIGNAL("accepted()"), SLOT("accept()")) - self.connect(bbox, SIGNAL("rejected()"), SLOT("reject()")) + apply_btn.clicked.connect(self.apply) + bbox.accepted.connect(self.accept) + bbox.rejected.connect(self.reject) layout.addWidget(bbox) self.setLayout(layout) @@ -481,11 +484,13 @@ btn = self.bbox.button(btn_type) if btn is not None: btn.setEnabled(valid) - + + @Slot() def accept(self): self.data = self.formwidget.get() QDialog.accept(self) - + + @Slot() def reject(self): self.data = None QDialog.reject(self) @@ -530,9 +535,13 @@ """ # Create a QApplication instance if no instance currently exists # (e.g. if the module is used directly from the interpreter) - if QApplication.startingUp(): + test_travis = os.environ.get('TEST_CI_WIDGETS', None) + if test_travis is not None: + from spyder.utils.qthelpers import qapplication + _app = qapplication(test_time=1) + elif QApplication.startingUp(): _app = QApplication([]) - + dialog = FormDialog(data, title, comment, icon, parent, apply) if dialog.exec_(): return dialog.get() diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/widgets/helperwidgets.py spyder-3.0.2+dfsg1/spyder/widgets/helperwidgets.py --- spyder-2.3.8+dfsg1/spyder/widgets/helperwidgets.py 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/widgets/helperwidgets.py 2016-10-25 02:05:23.000000000 +0200 @@ -0,0 +1,260 @@ +# -*- coding: utf-8 -*- +# +# Copyright © Spyder Project Contributors +# Licensed under the terms of the MIT License +# (see spyder/__init__.py for details) + +""" +Helper widgets. +""" + +# Third party imports +from qtpy.QtCore import QPoint, QSize, Qt +from qtpy.QtGui import QAbstractTextDocumentLayout, QPainter, QTextDocument +from qtpy.QtWidgets import (QApplication, QCheckBox, QLineEdit, QMessageBox, + QSpacerItem, QStyle, QStyledItemDelegate, + QStyleOptionViewItem, QToolButton, QToolTip, + QVBoxLayout) + +# Local imports +from spyder.config.base import _ +from spyder.utils import icon_manager as ima +from spyder.utils.qthelpers import get_std_icon + + +class HelperToolButton(QToolButton): + """Subclasses QToolButton, to provide a simple tooltip on mousedown. + """ + def __init__(self): + QToolButton.__init__(self) + self.setIcon(get_std_icon('MessageBoxInformation')) + style = """ + QToolButton { + border: 1px solid grey; + padding:0px; + border-radius: 2px; + background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #f6f7fa, stop: 1 #dadbde); + } + """ + self.setStyleSheet(style) + + def setToolTip(self, text): + self._tip_text = text + + def toolTip(self): + return self._tip_text + + def mousePressEvent(self, event): + QToolTip.hideText() + + def mouseReleaseEvent(self, event): + QToolTip.showText(self.mapToGlobal(QPoint(0, self.height())), + self._tip_text) + + +class MessageCheckBox(QMessageBox): + """ + A QMessageBox derived widget that includes a QCheckBox aligned to the right + under the message and on top of the buttons. + """ + def __init__(self, *args, **kwargs): + super(MessageCheckBox, self).__init__(*args, **kwargs) + + self._checkbox = QCheckBox() + + # Set layout to include checkbox + size = 9 + check_layout = QVBoxLayout() + check_layout.addItem(QSpacerItem(size, size)) + check_layout.addWidget(self._checkbox, 0, Qt.AlignRight) + check_layout.addItem(QSpacerItem(size, size)) + + # Access the Layout of the MessageBox to add the Checkbox + layout = self.layout() + layout.addLayout(check_layout, 1, 1) + + # --- Public API + # Methods to access the checkbox + def is_checked(self): + return self._checkbox.isChecked() + + def set_checked(self, value): + return self._checkbox.setChecked(value) + + def set_check_visible(self, value): + self._checkbox.setVisible(value) + + def is_check_visible(self): + self._checkbox.isVisible() + + def checkbox_text(self): + self._checkbox.text() + + def set_checkbox_text(self, text): + self._checkbox.setText(text) + + +class HTMLDelegate(QStyledItemDelegate): + """With this delegate, a QListWidgetItem or a QTableItem can render HTML. + + Taken from http://stackoverflow.com/a/5443112/2399799 + """ + def __init__(self, parent, margin=0): + super(HTMLDelegate, self).__init__(parent) + self._margin = margin + + def paint(self, painter, option, index): + options = QStyleOptionViewItem(option) + self.initStyleOption(options, index) + + style = (QApplication.style() if options.widget is None + else options.widget.style()) + + doc = QTextDocument() + doc.setDocumentMargin(self._margin) + doc.setHtml(options.text) + + options.text = "" + style.drawControl(QStyle.CE_ItemViewItem, options, painter) + + ctx = QAbstractTextDocumentLayout.PaintContext() + + textRect = style.subElementRect(QStyle.SE_ItemViewItemText, options) + painter.save() + + # Adjustments for the file switcher + if hasattr(options.widget, 'files_list'): + if style.objectName() in ['oxygen', 'qtcurve', 'breeze']: + if options.widget.files_list: + painter.translate(textRect.topLeft() + QPoint(4, -9)) + else: + painter.translate(textRect.topLeft()) + else: + if options.widget.files_list: + painter.translate(textRect.topLeft() + QPoint(4, 4)) + else: + painter.translate(textRect.topLeft() + QPoint(2, 4)) + else: + painter.translate(textRect.topLeft() + QPoint(0, -3)) + doc.documentLayout().draw(painter, ctx) + painter.restore() + + def sizeHint(self, option, index): + options = QStyleOptionViewItem(option) + self.initStyleOption(options, index) + + doc = QTextDocument() + doc.setHtml(options.text) + + return QSize(doc.idealWidth(), doc.size().height() - 2) + + +class IconLineEdit(QLineEdit): + """Custom QLineEdit that includes an icon representing the validation.""" + + def __init__(self, *args, **kwargs): + super(IconLineEdit, self).__init__(*args, **kwargs) + + self._status = True + self._status_set = True + self._valid_icon = ima.icon('todo') + self._invalid_icon = ima.icon('warning') + self._set_icon = ima.icon('todo_list') + self._application_style = QApplication.style().objectName() + self._refresh() + self._paint_count = 0 + self._icon_visible = False + + def _refresh(self): + """After an application style change, the paintEvent updates the + custom defined stylesheet. + """ + padding = self.height() + css_base = """QLineEdit {{ + border: none; + padding-right: {padding}px; + }} + """ + css_oxygen = """QLineEdit {{background: transparent; + border: none; + padding-right: {padding}px; + }} + """ + if self._application_style == 'oxygen': + css_template = css_oxygen + else: + css_template = css_base + + css = css_template.format(padding=padding) + self.setStyleSheet(css) + self.update() + + def hide_status_icon(self): + """Show the status icon.""" + self._icon_visible = False + self.repaint() + self.update() + + def show_status_icon(self): + """Hide the status icon.""" + self._icon_visible = True + self.repaint() + self.update() + + def update_status(self, value, value_set): + """Update the status and set_status to update the icons to display.""" + self._status = value + self._status_set = value_set + self.repaint() + self.update() + + def paintEvent(self, event): + """Qt Override. + + Include a validation icon to the left of the line edit. + """ + super(IconLineEdit, self).paintEvent(event) + painter = QPainter(self) + + rect = self.geometry() + space = int((rect.height())/6) + h = rect.height() - space + w = rect.width() - h + + if self._icon_visible: + if self._status and self._status_set: + pixmap = self._set_icon.pixmap(h, h) + elif self._status: + pixmap = self._valid_icon.pixmap(h, h) + else: + pixmap = self._invalid_icon.pixmap(h, h) + + painter.drawPixmap(w, space, pixmap) + + application_style = QApplication.style().objectName() + if self._application_style != application_style: + self._application_style = application_style + self._refresh() + + # Small hack to gurantee correct padding on Spyder start + if self._paint_count < 5: + self._paint_count += 1 + self._refresh() + + +def test_msgcheckbox(): + from spyder.utils.qthelpers import qapplication + app = qapplication() + box = MessageCheckBox() + box.setWindowTitle(_("Spyder updates")) + box.setText("Testing checkbox") + box.set_checkbox_text("Check for updates on startup?") + box.setStandardButtons(QMessageBox.Ok) + box.setDefaultButton(QMessageBox.Ok) + box.setIcon(QMessageBox.Information) + box.exec_() + + +if __name__ == '__main__': + test_msgcheckbox() diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/widgets/importwizard.py spyder-3.0.2+dfsg1/spyder/widgets/importwizard.py --- spyder-2.3.8+dfsg1/spyder/widgets/importwizard.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/widgets/importwizard.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,637 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright © 2009-2010 Pierre Raybaut -# Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) - -""" -Text data Importing Wizard based on Qt -""" - -from __future__ import print_function - -from spyderlib.qt.QtGui import (QTableView, QVBoxLayout, QHBoxLayout, - QGridLayout, QWidget, QDialog, QTextEdit, - QTabWidget, QPushButton, QLabel, QSpacerItem, - QSizePolicy, QCheckBox, QColor, QRadioButton, - QLineEdit, QFrame, QMenu, QIntValidator, - QGroupBox, QMessageBox) -from spyderlib.qt.QtCore import (Qt, QModelIndex, QAbstractTableModel, - SIGNAL, SLOT, Slot) -from spyderlib.qt.compat import to_qvariant - -from functools import partial as ft_partial - -try: - import pandas as pd -except ImportError: - pd = None - -# Local import -from spyderlib.baseconfig import _ -from spyderlib.utils import programs -from spyderlib.utils.qthelpers import get_icon, add_actions, create_action -from spyderlib.py3compat import (TEXT_TYPES, INT_TYPES, to_text_string, u, - zip_longest, io) - -def try_to_parse(value): - _types = ('int', 'float') - for _t in _types: - try: - _val = eval("%s('%s')" % (_t, value)) - return _val - except (ValueError, SyntaxError): - pass - return value - -def try_to_eval(value): - try: - return eval(value) - except (NameError, SyntaxError, ImportError): - return value - -#----Numpy arrays support -class FakeObject(object): - """Fake class used in replacement of missing modules""" - pass -try: - from numpy import ndarray, array -except ImportError: - class ndarray(FakeObject): # analysis:ignore - """Fake ndarray""" - pass - -#----date and datetime objects support -import datetime -try: - from dateutil.parser import parse as dateparse -except ImportError: - def dateparse(datestr, dayfirst=True): # analysis:ignore - """Just for 'day/month/year' strings""" - _a, _b, _c = list(map(int, datestr.split('/'))) - if dayfirst: - return datetime.datetime(_c, _b, _a) - return datetime.datetime(_c, _a, _b) - -def datestr_to_datetime(value, dayfirst=True): - return dateparse(value, dayfirst=dayfirst) - -#----Background colors for supported types -COLORS = { - bool: Qt.magenta, - tuple([float] + list(INT_TYPES)): Qt.blue, - list: Qt.yellow, - dict: Qt.cyan, - tuple: Qt.lightGray, - TEXT_TYPES: Qt.darkRed, - ndarray: Qt.green, - datetime.date: Qt.darkYellow, - } - -def get_color(value, alpha): - """Return color depending on value type""" - color = QColor() - for typ in COLORS: - if isinstance(value, typ): - color = QColor(COLORS[typ]) - color.setAlphaF(alpha) - return color - -class ContentsWidget(QWidget): - """Import wizard contents widget""" - def __init__(self, parent, text): - QWidget.__init__(self, parent) - - self.text_editor = QTextEdit(self) - self.text_editor.setText(text) - self.text_editor.setReadOnly(True) - - # Type frame - type_layout = QHBoxLayout() - type_label = QLabel(_("Import as")) - type_layout.addWidget(type_label) - data_btn = QRadioButton(_("data")) - data_btn.setChecked(True) - self._as_data= True - type_layout.addWidget(data_btn) - code_btn = QRadioButton(_("code")) - self._as_code = False - type_layout.addWidget(code_btn) - txt_btn = QRadioButton(_("text")) - type_layout.addWidget(txt_btn) - - h_spacer = QSpacerItem(40, 20, - QSizePolicy.Expanding, QSizePolicy.Minimum) - type_layout.addItem(h_spacer) - type_frame = QFrame() - type_frame.setLayout(type_layout) - - # Opts frame - grid_layout = QGridLayout() - grid_layout.setSpacing(0) - - col_label = QLabel(_("Column separator:")) - grid_layout.addWidget(col_label, 0, 0) - col_w = QWidget() - col_btn_layout = QHBoxLayout() - self.tab_btn = QRadioButton(_("Tab")) - self.tab_btn.setChecked(False) - col_btn_layout.addWidget(self.tab_btn) - other_btn_col = QRadioButton(_("other")) - other_btn_col.setChecked(True) - col_btn_layout.addWidget(other_btn_col) - col_w.setLayout(col_btn_layout) - grid_layout.addWidget(col_w, 0, 1) - self.line_edt = QLineEdit(",") - self.line_edt.setMaximumWidth(30) - self.line_edt.setEnabled(True) - self.connect(other_btn_col, SIGNAL("toggled(bool)"), - self.line_edt, SLOT("setEnabled(bool)")) - grid_layout.addWidget(self.line_edt, 0, 2) - - row_label = QLabel(_("Row separator:")) - grid_layout.addWidget(row_label, 1, 0) - row_w = QWidget() - row_btn_layout = QHBoxLayout() - self.eol_btn = QRadioButton(_("EOL")) - self.eol_btn.setChecked(True) - row_btn_layout.addWidget(self.eol_btn) - other_btn_row = QRadioButton(_("other")) - row_btn_layout.addWidget(other_btn_row) - row_w.setLayout(row_btn_layout) - grid_layout.addWidget(row_w, 1, 1) - self.line_edt_row = QLineEdit(";") - self.line_edt_row.setMaximumWidth(30) - self.line_edt_row.setEnabled(False) - self.connect(other_btn_row, SIGNAL("toggled(bool)"), - self.line_edt_row, SLOT("setEnabled(bool)")) - grid_layout.addWidget(self.line_edt_row, 1, 2) - - grid_layout.setRowMinimumHeight(2, 15) - - other_group = QGroupBox(_("Additional options")) - other_layout = QGridLayout() - other_group.setLayout(other_layout) - - skiprows_label = QLabel(_("Skip rows:")) - other_layout.addWidget(skiprows_label, 0, 0) - self.skiprows_edt = QLineEdit('0') - self.skiprows_edt.setMaximumWidth(30) - intvalid = QIntValidator(0, len(to_text_string(text).splitlines()), - self.skiprows_edt) - self.skiprows_edt.setValidator(intvalid) - other_layout.addWidget(self.skiprows_edt, 0, 1) - - other_layout.setColumnMinimumWidth(2, 5) - - comments_label = QLabel(_("Comments:")) - other_layout.addWidget(comments_label, 0, 3) - self.comments_edt = QLineEdit('#') - self.comments_edt.setMaximumWidth(30) - other_layout.addWidget(self.comments_edt, 0, 4) - - self.trnsp_box = QCheckBox(_("Transpose")) - #self.trnsp_box.setEnabled(False) - other_layout.addWidget(self.trnsp_box, 1, 0, 2, 0) - - grid_layout.addWidget(other_group, 3, 0, 2, 0) - - opts_frame = QFrame() - opts_frame.setLayout(grid_layout) - - self.connect(data_btn, SIGNAL("toggled(bool)"), - opts_frame, SLOT("setEnabled(bool)")) - self.connect(data_btn, SIGNAL("toggled(bool)"), - self, SLOT("set_as_data(bool)")) - self.connect(code_btn, SIGNAL("toggled(bool)"), - self, SLOT("set_as_code(bool)")) -# self.connect(txt_btn, SIGNAL("toggled(bool)"), -# self, SLOT("is_text(bool)")) - - # Final layout - layout = QVBoxLayout() - layout.addWidget(type_frame) - layout.addWidget(self.text_editor) - layout.addWidget(opts_frame) - self.setLayout(layout) - - def get_as_data(self): - """Return if data type conversion""" - return self._as_data - - def get_as_code(self): - """Return if code type conversion""" - return self._as_code - - def get_as_num(self): - """Return if numeric type conversion""" - return self._as_num - - def get_col_sep(self): - """Return the column separator""" - if self.tab_btn.isChecked(): - return u("\t") - return to_text_string(self.line_edt.text()) - - def get_row_sep(self): - """Return the row separator""" - if self.eol_btn.isChecked(): - return u("\n") - return to_text_string(self.line_edt_row.text()) - - def get_skiprows(self): - """Return number of lines to be skipped""" - return int(to_text_string(self.skiprows_edt.text())) - - def get_comments(self): - """Return comment string""" - return to_text_string(self.comments_edt.text()) - - @Slot(bool) - def set_as_data(self, as_data): - """Set if data type conversion""" - self._as_data = as_data - self.emit(SIGNAL("asDataChanged(bool)"), as_data) - - @Slot(bool) - def set_as_code(self, as_code): - """Set if code type conversion""" - self._as_code = as_code - - -class PreviewTableModel(QAbstractTableModel): - """Import wizard preview table model""" - def __init__(self, data=[], parent=None): - QAbstractTableModel.__init__(self, parent) - self._data = data - - def rowCount(self, parent=QModelIndex()): - """Return row count""" - return len(self._data) - - def columnCount(self, parent=QModelIndex()): - """Return column count""" - return len(self._data[0]) - - def _display_data(self, index): - """Return a data element""" - return to_qvariant(self._data[index.row()][index.column()]) - - def data(self, index, role=Qt.DisplayRole): - """Return a model data element""" - if not index.isValid(): - return to_qvariant() - if role == Qt.DisplayRole: - return self._display_data(index) - elif role == Qt.BackgroundColorRole: - return to_qvariant(get_color(self._data[index.row()][index.column()], .2)) - elif role == Qt.TextAlignmentRole: - return to_qvariant(int(Qt.AlignRight|Qt.AlignVCenter)) - return to_qvariant() - - def setData(self, index, value, role=Qt.EditRole): - """Set model data""" - return False - - def get_data(self): - """Return a copy of model data""" - return self._data[:][:] - - def parse_data_type(self, index, **kwargs): - """Parse a type to an other type""" - if not index.isValid(): - return False - try: - if kwargs['atype'] == "date": - self._data[index.row()][index.column()] = \ - datestr_to_datetime(self._data[index.row()][index.column()], - kwargs['dayfirst']).date() - elif kwargs['atype'] == "perc": - _tmp = self._data[index.row()][index.column()].replace("%", "") - self._data[index.row()][index.column()] = eval(_tmp)/100. - elif kwargs['atype'] == "account": - _tmp = self._data[index.row()][index.column()].replace(",", "") - self._data[index.row()][index.column()] = eval(_tmp) - elif kwargs['atype'] == "unicode": - self._data[index.row()][index.column()] = to_text_string( - self._data[index.row()][index.column()]) - elif kwargs['atype'] == "int": - self._data[index.row()][index.column()] = int( - self._data[index.row()][index.column()]) - elif kwargs['atype'] == "float": - self._data[index.row()][index.column()] = float( - self._data[index.row()][index.column()]) - self.emit(SIGNAL("dataChanged(QModelIndex,QModelIndex)"), index, index) - except Exception as instance: - print(instance) - -class PreviewTable(QTableView): - """Import wizard preview widget""" - def __init__(self, parent): - QTableView.__init__(self, parent) - self._model = None - - # Setting up actions - self.date_dayfirst_action = create_action(self, "dayfirst", - triggered=ft_partial(self.parse_to_type, atype="date", dayfirst=True)) - self.date_monthfirst_action = create_action(self, "monthfirst", - triggered=ft_partial(self.parse_to_type, atype="date", dayfirst=False)) - self.perc_action = create_action(self, "perc", - triggered=ft_partial(self.parse_to_type, atype="perc")) - self.acc_action = create_action(self, "account", - triggered=ft_partial(self.parse_to_type, atype="account")) - self.str_action = create_action(self, "unicode", - triggered=ft_partial(self.parse_to_type, atype="unicode")) - self.int_action = create_action(self, "int", - triggered=ft_partial(self.parse_to_type, atype="int")) - self.float_action = create_action(self, "float", - triggered=ft_partial(self.parse_to_type, atype="float")) - - # Setting up menus - self.date_menu = QMenu() - self.date_menu.setTitle("Date") - add_actions( self.date_menu, (self.date_dayfirst_action, - self.date_monthfirst_action)) - self.parse_menu = QMenu(self) - self.parse_menu.addMenu(self.date_menu) - add_actions( self.parse_menu, (self.perc_action, self.acc_action)) - self.parse_menu.setTitle("String to") - self.opt_menu = QMenu(self) - self.opt_menu.addMenu(self.parse_menu) - add_actions( self.opt_menu, (self.str_action, self.int_action, - self.float_action)) - - def _shape_text(self, text, colsep=u("\t"), rowsep=u("\n"), - transpose=False, skiprows=0, comments='#'): - """Decode the shape of the given text""" - assert colsep != rowsep - out = [] - text_rows = text.split(rowsep)[skiprows:] - for row in text_rows: - stripped = to_text_string(row).strip() - if len(stripped) == 0 or stripped.startswith(comments): - continue - line = to_text_string(row).split(colsep) - line = [try_to_parse(to_text_string(x)) for x in line] - out.append(line) - # Replace missing elements with np.nan's or None's - if programs.is_module_installed('numpy'): - from numpy import nan - out = list(zip_longest(*out, fillvalue=nan)) - else: - out = list(zip_longest(*out, fillvalue=None)) - # Tranpose the last result to get the expected one - out = [[r[col] for r in out] for col in range(len(out[0]))] - if transpose: - return [[r[col] for r in out] for col in range(len(out[0]))] - return out - - def get_data(self): - """Return model data""" - if self._model is None: - return None - return self._model.get_data() - - def process_data(self, text, colsep=u("\t"), rowsep=u("\n"), - transpose=False, skiprows=0, comments='#'): - """Put data into table model""" - data = self._shape_text(text, colsep, rowsep, transpose, skiprows, - comments) - self._model = PreviewTableModel(data) - self.setModel(self._model) - - def parse_to_type(self,**kwargs): - """Parse to a given type""" - indexes = self.selectedIndexes() - if not indexes: return - for index in indexes: - self.model().parse_data_type(index, **kwargs) - - def contextMenuEvent(self, event): - """Reimplement Qt method""" - self.opt_menu.popup(event.globalPos()) - event.accept() - -class PreviewWidget(QWidget): - """Import wizard preview widget""" - - def __init__(self, parent): - QWidget.__init__(self, parent) - - vert_layout = QVBoxLayout() - - # Type frame - type_layout = QHBoxLayout() - type_label = QLabel(_("Import as")) - type_layout.addWidget(type_label) - - self.array_btn = array_btn = QRadioButton(_("array")) - array_btn.setEnabled(ndarray is not FakeObject) - array_btn.setChecked(ndarray is not FakeObject) - type_layout.addWidget(array_btn) - - list_btn = QRadioButton(_("list")) - list_btn.setChecked(not array_btn.isChecked()) - type_layout.addWidget(list_btn) - - if pd: - self.df_btn = df_btn = QRadioButton(_("DataFrame")) - df_btn.setChecked(False) - type_layout.addWidget(df_btn) - - h_spacer = QSpacerItem(40, 20, - QSizePolicy.Expanding, QSizePolicy.Minimum) - type_layout.addItem(h_spacer) - type_frame = QFrame() - type_frame.setLayout(type_layout) - - self._table_view = PreviewTable(self) - vert_layout.addWidget(type_frame) - vert_layout.addWidget(self._table_view) - self.setLayout(vert_layout) - - def open_data(self, text, colsep=u("\t"), rowsep=u("\n"), - transpose=False, skiprows=0, comments='#'): - """Open clipboard text as table""" - if pd: - self.pd_text = text - self.pd_info = dict(sep=colsep, lineterminator=rowsep, - skiprows=skiprows,comment=comments) - self._table_view.process_data(text, colsep, rowsep, transpose, - skiprows, comments) - - def get_data(self): - """Return table data""" - return self._table_view.get_data() - -class ImportWizard(QDialog): - """Text data import wizard""" - def __init__(self, parent, text, - title=None, icon=None, contents_title=None, varname=None): - QDialog.__init__(self, parent) - - # Destroying the C++ object right after closing the dialog box, - # otherwise it may be garbage-collected in another QThread - # (e.g. the editor's analysis thread in Spyder), thus leading to - # a segmentation fault on UNIX or an application crash on Windows - self.setAttribute(Qt.WA_DeleteOnClose) - - if title is None: - title = _("Import wizard") - self.setWindowTitle(title) - if icon is None: - self.setWindowIcon(get_icon("fileimport.png")) - if contents_title is None: - contents_title = _("Raw text") - - if varname is None: - varname = _("variable_name") - - self.var_name, self.clip_data = None, None - - # Setting GUI - self.tab_widget = QTabWidget(self) - self.text_widget = ContentsWidget(self, text) - self.table_widget = PreviewWidget(self) - - self.tab_widget.addTab(self.text_widget, _("text")) - self.tab_widget.setTabText(0, contents_title) - self.tab_widget.addTab(self.table_widget, _("table")) - self.tab_widget.setTabText(1, _("Preview")) - self.tab_widget.setTabEnabled(1, False) - - name_layout = QHBoxLayout() - name_label = QLabel(_("Variable Name")) - name_layout.addWidget(name_label) - - self.name_edt = QLineEdit() - self.name_edt.setText(varname) - name_layout.addWidget(self.name_edt) - - btns_layout = QHBoxLayout() - cancel_btn = QPushButton(_("Cancel")) - btns_layout.addWidget(cancel_btn) - self.connect(cancel_btn, SIGNAL("clicked()"), self, SLOT("reject()")) - h_spacer = QSpacerItem(40, 20, - QSizePolicy.Expanding, QSizePolicy.Minimum) - btns_layout.addItem(h_spacer) - self.back_btn = QPushButton(_("Previous")) - self.back_btn.setEnabled(False) - btns_layout.addWidget(self.back_btn) - self.connect(self.back_btn, SIGNAL("clicked()"), - ft_partial(self._set_step, step=-1)) - self.fwd_btn = QPushButton(_("Next")) - btns_layout.addWidget(self.fwd_btn) - self.connect(self.fwd_btn, SIGNAL("clicked()"), - ft_partial(self._set_step, step=1)) - self.done_btn = QPushButton(_("Done")) - self.done_btn.setEnabled(False) - btns_layout.addWidget(self.done_btn) - self.connect(self.done_btn, SIGNAL("clicked()"), - self, SLOT("process()")) - - self.connect(self.text_widget, SIGNAL("asDataChanged(bool)"), - self.fwd_btn, SLOT("setEnabled(bool)")) - self.connect(self.text_widget, SIGNAL("asDataChanged(bool)"), - self.done_btn, SLOT("setDisabled(bool)")) - layout = QVBoxLayout() - layout.addLayout(name_layout) - layout.addWidget(self.tab_widget) - layout.addLayout(btns_layout) - self.setLayout(layout) - - def _focus_tab(self, tab_idx): - """Change tab focus""" - for i in range(self.tab_widget.count()): - self.tab_widget.setTabEnabled(i, False) - self.tab_widget.setTabEnabled(tab_idx, True) - self.tab_widget.setCurrentIndex(tab_idx) - - def _set_step(self, step): - """Proceed to a given step""" - new_tab = self.tab_widget.currentIndex() + step - assert new_tab < self.tab_widget.count() and new_tab >= 0 - if new_tab == self.tab_widget.count()-1: - try: - self.table_widget.open_data(self._get_plain_text(), - self.text_widget.get_col_sep(), - self.text_widget.get_row_sep(), - self.text_widget.trnsp_box.isChecked(), - self.text_widget.get_skiprows(), - self.text_widget.get_comments()) - self.done_btn.setEnabled(True) - self.done_btn.setDefault(True) - self.fwd_btn.setEnabled(False) - self.back_btn.setEnabled(True) - except (SyntaxError, AssertionError) as error: - QMessageBox.critical(self, _("Import wizard"), - _("Unable to proceed to next step" - "

    Please check your entries." - "

    Error message:
    %s") % str(error)) - return - elif new_tab == 0: - self.done_btn.setEnabled(False) - self.fwd_btn.setEnabled(True) - self.back_btn.setEnabled(False) - self._focus_tab(new_tab) - - def get_data(self): - """Return processed data""" - # It is import to avoid accessing Qt C++ object as it has probably - # already been destroyed, due to the Qt.WA_DeleteOnClose attribute - return self.var_name, self.clip_data - - def _simplify_shape(self, alist, rec=0): - """Reduce the alist dimension if needed""" - if rec != 0: - if len(alist) == 1: - return alist[-1] - return alist - if len(alist) == 1: - return self._simplify_shape(alist[-1], 1) - return [self._simplify_shape(al, 1) for al in alist] - - def _get_table_data(self): - """Return clipboard processed as data""" - data = self._simplify_shape( - self.table_widget.get_data()) - if self.table_widget.array_btn.isChecked(): - return array(data) - elif pd and self.table_widget.df_btn.isChecked(): - info = self.table_widget.pd_info - buf = io.StringIO(self.table_widget.pd_text) - return pd.read_csv(buf, **info) - return data - - def _get_plain_text(self): - """Return clipboard as text""" - return self.text_widget.text_editor.toPlainText() - - @Slot() - def process(self): - """Process the data from clipboard""" - var_name = self.name_edt.text() - try: - self.var_name = str(var_name) - except UnicodeEncodeError: - self.var_name = to_text_string(var_name) - if self.text_widget.get_as_data(): - self.clip_data = self._get_table_data() - elif self.text_widget.get_as_code(): - self.clip_data = try_to_eval( - to_text_string(self._get_plain_text())) - else: - self.clip_data = to_text_string(self._get_plain_text()) - self.accept() - - -def test(text): - """Test""" - from spyderlib.utils.qthelpers import qapplication - _app = qapplication() # analysis:ignore - dialog = ImportWizard(None, text) - if dialog.exec_(): - print(dialog.get_data()) - -if __name__ == "__main__": - test(u("17/11/1976\t1.34\n14/05/09\t3.14")) diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/widgets/__init__.py spyder-3.0.2+dfsg1/spyder/widgets/__init__.py --- spyder-2.3.8+dfsg1/spyder/widgets/__init__.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/widgets/__init__.py 2016-10-25 02:05:23.000000000 +0200 @@ -1,15 +1,15 @@ # -*- coding: utf-8 -*- # -# Copyright © 2009-2010 Pierre Raybaut +# Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) +# (see spyder/__init__.py for details) """ -spyderlib.widgets -================= +spyder.widgets +============== Widgets defined in this module may be used in any other Qt-based application They are also used in Spyder through the Plugin interface -(see spyderlib.plugins) +(see spyder.plugins) """ diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/widgets/internalshell.py spyder-3.0.2+dfsg1/spyder/widgets/internalshell.py --- spyder-2.3.8+dfsg1/spyder/widgets/internalshell.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/widgets/internalshell.py 2016-11-08 01:45:11.000000000 +0100 @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- # -# Copyright © 2009-2010 Pierre Raybaut +# Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) +# (see spyder/__init__.py for details) """Internal shell widget : PythonShellWidget + Interpreter""" @@ -13,32 +13,35 @@ #FIXME: Internal shell MT: for i in range(100000): print i -> bug -#----Builtins* -from spyderlib.py3compat import builtins -from spyderlib.widgets.objecteditor import oedit -builtins.oedit = oedit - +# Standard library imports +from time import time import os import threading -from time import time -from subprocess import Popen - -from spyderlib.qt.QtGui import QMessageBox -from spyderlib.qt.QtCore import SIGNAL, QObject, QEventLoop -# Local import -from spyderlib import get_versions -from spyderlib.utils.qthelpers import create_action, get_std_icon -from spyderlib.interpreter import Interpreter -from spyderlib.utils.dochelpers import getargtxt, getsource, getdoc, getobjdir -from spyderlib.utils.misc import get_error_match -#TODO: remove the CONF object and make it work anyway -# In fact, this 'CONF' object has nothing to do in package spyderlib.widgets +# Third party imports +from qtpy.QtCore import QEventLoop, QObject, Signal, Slot +from qtpy.QtWidgets import QMessageBox + +# Local imports +from spyder import get_versions +from spyder.interpreter import Interpreter +from spyder.py3compat import (builtins, getcwd, to_binary_string, + to_text_string, u) +from spyder.utils import icon_manager as ima +from spyder.utils import programs +from spyder.utils.dochelpers import getargtxt, getdoc, getobjdir, getsource +from spyder.utils.misc import get_error_match +from spyder.utils.qthelpers import create_action +from spyder.widgets.shell import PythonShellWidget +from spyder.widgets.variableexplorer.objecteditor import oedit +# TODO: remove the CONF object and make it work anyway +# In fact, this 'CONF' object has nothing to do in package spyder/widgets # which should not contain anything directly related to Spyder's main app -from spyderlib.baseconfig import get_conf_path, _, DEBUG -from spyderlib.config import CONF -from spyderlib.widgets.shell import PythonShellWidget -from spyderlib.py3compat import to_text_string, getcwd, to_binary_string, u +from spyder.config.base import _, DEBUG, get_conf_path +from spyder.config.main import CONF + + +builtins.oedit = oedit def create_banner(message): @@ -53,6 +56,8 @@ class SysOutput(QObject): """Handle standard I/O queue""" + data_avail = Signal() + def __init__(self): QObject.__init__(self) self.queue = [] @@ -62,7 +67,7 @@ self.lock.acquire() self.queue.append(val) self.lock.release() - self.emit(SIGNAL("void data_avail()")) + self.data_avail.emit() def empty_queue(self): self.lock.acquire() @@ -75,24 +80,35 @@ def flush(self): pass + # This is needed to fix Issue 2984 + @property + def closed(self): + return False + class WidgetProxyData(object): pass class WidgetProxy(QObject): """Handle Shell widget refresh signal""" + + sig_new_prompt = Signal(str) + sig_set_readonly = Signal(bool) + sig_edit = Signal(str, bool) + sig_wait_input = Signal(str) + def __init__(self, input_condition): QObject.__init__(self) self.input_data = None self.input_condition = input_condition def new_prompt(self, prompt): - self.emit(SIGNAL("new_prompt(QString)"), prompt) + self.sig_new_prompt.emit(prompt) def set_readonly(self, state): - self.emit(SIGNAL("set_readonly(bool)"), state) + self.sig_set_readonly.emit(state) def edit(self, filename, external_editor=False): - self.emit(SIGNAL("edit(QString,bool)"), filename, external_editor) + self.sig_edit.emit(filename, external_editor) def data_available(self): """Return True if input data is available""" @@ -100,7 +116,7 @@ def wait_input(self, prompt=''): self.input_data = WidgetProxyData - self.emit(SIGNAL("wait_input(QString)"), prompt) + self.sig_wait_input.emit(prompt) def end_input(self, cmd): self.input_condition.acquire() @@ -111,6 +127,12 @@ class InternalShell(PythonShellWidget): """Shell base widget: link between PythonShellWidget and Interpreter""" + + status = Signal(str) + refresh = Signal() + go_to_error = Signal(str) + focus_changed = Signal() + def __init__(self, parent=None, namespace=None, commands=[], message=None, max_line_count=300, font=None, exitfunc=None, profile=False, multithreaded=True, light_background=True): @@ -121,21 +143,17 @@ self.set_light_background(light_background) self.multithreaded = multithreaded self.setMaximumBlockCount(max_line_count) - - # For compatibility with ExtPythonShellWidget - self.is_ipykernel = False - + if font is not None: self.set_font(font) - + # Allow raw_input support: self.input_loop = None self.input_mode = False # KeyboardInterrupt support self.interrupted = False # used only for not-multithreaded mode - self.connect(self, SIGNAL("keyboard_interrupt()"), - self.keyboard_interrupt) + self.sig_keyboard_interrupt.connect(self.keyboard_interrupt) # Code completion / calltips getcfg = lambda option: CONF.get('internal_console', option) @@ -153,14 +171,12 @@ self.start_interpreter(namespace) # Clear status bar - self.emit(SIGNAL("status(QString)"), '') + self.status.emit('') # Embedded shell -- requires the monitor (which installs the # 'open_in_spyder' function in builtins) if hasattr(builtins, 'open_in_spyder'): - self.connect(self, SIGNAL("go_to_error(QString)"), - self.open_with_external_spyder) - + self.go_to_error.connect(self.open_with_external_spyder) #------ Interpreter def start_interpreter(self, namespace): @@ -171,18 +187,12 @@ self.interpreter.closing() self.interpreter = Interpreter(namespace, self.exitfunc, SysOutput, WidgetProxy, DEBUG) - self.connect(self.interpreter.stdout_write, - SIGNAL("void data_avail()"), self.stdout_avail) - self.connect(self.interpreter.stderr_write, - SIGNAL("void data_avail()"), self.stderr_avail) - self.connect(self.interpreter.widget_proxy, - SIGNAL("set_readonly(bool)"), self.setReadOnly) - self.connect(self.interpreter.widget_proxy, - SIGNAL("new_prompt(QString)"), self.new_prompt) - self.connect(self.interpreter.widget_proxy, - SIGNAL("edit(QString,bool)"), self.edit_script) - self.connect(self.interpreter.widget_proxy, - SIGNAL("wait_input(QString)"), self.wait_input) + self.interpreter.stdout_write.data_avail.connect(self.stdout_avail) + self.interpreter.stderr_write.data_avail.connect(self.stderr_avail) + self.interpreter.widget_proxy.sig_set_readonly.connect(self.setReadOnly) + self.interpreter.widget_proxy.sig_new_prompt.connect(self.new_prompt) + self.interpreter.widget_proxy.sig_edit.connect(self.edit_script) + self.interpreter.widget_proxy.sig_wait_input.connect(self.wait_input) if self.multithreaded: self.interpreter.start() @@ -196,7 +206,7 @@ # First prompt self.new_prompt(self.interpreter.p1) - self.emit(SIGNAL("refresh()")) + self.refresh.emit() return self.interpreter @@ -250,10 +260,11 @@ """Reimplement PythonShellWidget method""" PythonShellWidget.setup_context_menu(self) self.help_action = create_action(self, _("Help..."), - icon=get_std_icon('DialogHelpButton'), + icon=ima.icon('DialogHelpButton'), triggered=self.help) self.menu.addAction(self.help_action) + @Slot() def help(self): """Help on Spyder console""" QMessageBox.about(self, _("Help"), @@ -291,11 +302,10 @@ editor_path = CONF.get('internal_console', 'external_editor/path') goto_option = CONF.get('internal_console', 'external_editor/gotoline') try: + args = [filename] if goto > 0 and goto_option: - Popen(r'%s "%s" %s%d' % (editor_path, filename, - goto_option, goto)) - else: - Popen(r'%s "%s"' % (editor_path, filename)) + args.append('%s%d'.format(goto_option, goto)) + programs.run_program(editor_path, args) except OSError: self.write_error("External editor was not found:" " %s\n" % editor_path) @@ -400,7 +410,7 @@ self.interpreter.stdin_write.write(to_binary_string(cmd + '\n')) if not self.multithreaded: self.interpreter.run_line() - self.emit(SIGNAL("refresh()")) + self.refresh.emit() #------ Code completion / Calltips diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/widgets/ipythonconsole/client.py spyder-3.0.2+dfsg1/spyder/widgets/ipythonconsole/client.py --- spyder-2.3.8+dfsg1/spyder/widgets/ipythonconsole/client.py 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/widgets/ipythonconsole/client.py 2016-11-16 16:40:01.000000000 +0100 @@ -0,0 +1,400 @@ +# -*- coding:utf-8 -*- +# +# Copyright © Spyder Project Contributors +# Licensed under the terms of the MIT License +# (see spyder/__init__.py for details) + +""" +Client widget for the IPython Console + +This is the widget used on all its tabs +""" + +# Standard library imports +from __future__ import absolute_import # Fix for Issue 1356 + +import os +import os.path as osp +from string import Template +from threading import Thread +import time + +# Third party imports (qtpy) +from qtpy.QtCore import Qt, QUrl, Signal, Slot +from qtpy.QtGui import QKeySequence +from qtpy.QtWidgets import (QHBoxLayout, QMenu, QMessageBox, QToolButton, + QVBoxLayout, QWidget) + +# Local imports +from spyder.config.base import (_, get_conf_path, get_image_path, + get_module_source_path) +from spyder.config.gui import get_font, get_shortcut +from spyder.utils import icon_manager as ima +from spyder.utils.qthelpers import (add_actions, create_action, + create_toolbutton) +from spyder.widgets.browser import WebView +from spyder.widgets.mixins import SaveHistoryMixin +from spyder.widgets.ipythonconsole import ShellWidget + + +#----------------------------------------------------------------------------- +# Templates +#----------------------------------------------------------------------------- +# Using the same css file from the Help plugin for now. Maybe +# later it'll be a good idea to create a new one. +UTILS_PATH = get_module_source_path('spyder', 'utils') +CSS_PATH = osp.join(UTILS_PATH, 'help', 'static', 'css') +TEMPLATES_PATH = osp.join(UTILS_PATH, 'ipython', 'templates') + +BLANK = open(osp.join(TEMPLATES_PATH, 'blank.html')).read() +LOADING = open(osp.join(TEMPLATES_PATH, 'loading.html')).read() +KERNEL_ERROR = open(osp.join(TEMPLATES_PATH, 'kernel_error.html')).read() + + +#----------------------------------------------------------------------------- +# Auxiliary functions +#----------------------------------------------------------------------------- +def background(f): + """ + Call a function in a simple thread, to prevent blocking + + Taken from the Jupyter Qtconsole project + """ + t = Thread(target=f) + t.start() + return t + + +#----------------------------------------------------------------------------- +# Client widget +#----------------------------------------------------------------------------- +class ClientWidget(QWidget, SaveHistoryMixin): + """ + Client widget for the IPython Console + + This is a widget composed of a shell widget and a WebView info widget + to print different messages there. + """ + + SEPARATOR = '%s##---(%s)---' % (os.linesep*2, time.ctime()) + append_to_history = Signal(str, str) + + def __init__(self, plugin, name, history_filename, config_options, + additional_options, interpreter_versions, + connection_file=None, hostname=None, + menu_actions=None, slave=False, + external_kernel=False): + super(ClientWidget, self).__init__(plugin) + SaveHistoryMixin.__init__(self) + + # --- Init attrs + self.name = name + self.history_filename = get_conf_path(history_filename) + self.connection_file = connection_file + self.hostname = hostname + self.menu_actions = menu_actions + self.slave = slave + + # --- Other attrs + self.options_button = None + self.stop_button = None + self.stop_icon = ima.icon('stop') + self.history = [] + + # --- Widgets + self.shellwidget = ShellWidget(config=config_options, + additional_options=additional_options, + interpreter_versions=interpreter_versions, + external_kernel=external_kernel, + local_kernel=True) + self.shellwidget.hide() + self.infowidget = WebView(self) + self.set_infowidget_font() + self.loading_page = self._create_loading_page() + self.infowidget.setHtml(self.loading_page, + QUrl.fromLocalFile(CSS_PATH)) + + # --- Layout + vlayout = QVBoxLayout() + toolbar_buttons = self.get_toolbar_buttons() + hlayout = QHBoxLayout() + for button in toolbar_buttons: + hlayout.addWidget(button) + vlayout.addLayout(hlayout) + vlayout.setContentsMargins(0, 0, 0, 0) + vlayout.addWidget(self.shellwidget) + vlayout.addWidget(self.infowidget) + self.setLayout(vlayout) + + # --- Exit function + self.exit_callback = lambda: plugin.close_client(client=self) + + # --- Signals + # As soon as some content is printed in the console, stop + # our loading animation + document = self.get_control().document() + document.contentsChange.connect(self._stop_loading_animation) + + #------ Public API -------------------------------------------------------- + def configure_shellwidget(self, give_focus=True): + """Configure shellwidget after kernel is started""" + if give_focus: + self.get_control().setFocus() + + # Connect shellwidget to the client + self.shellwidget.set_ipyclient(self) + + # To save history + self.shellwidget.executing.connect(self.add_to_history) + + # For Mayavi to run correctly + self.shellwidget.executing.connect(self.set_backend_for_mayavi) + + # To update history after execution + self.shellwidget.executed.connect(self.update_history) + + # To update the Variable Explorer after execution + self.shellwidget.executed.connect( + self.shellwidget.refresh_namespacebrowser) + + # To enable the stop button when executing a process + self.shellwidget.executing.connect(self.enable_stop_button) + + # To disable the stop button after execution stopped + self.shellwidget.executed.connect(self.disable_stop_button) + + def enable_stop_button(self): + self.stop_button.setEnabled(True) + + def disable_stop_button(self): + self.stop_button.setDisabled(True) + + @Slot() + def stop_button_click_handler(self): + """Method to handle what to do when the stop button is pressed""" + self.stop_button.setDisabled(True) + # Interrupt computations or stop debugging + if not self.shellwidget._reading: + self.interrupt_kernel() + else: + self.shellwidget.write_to_stdin('exit') + + def show_kernel_error(self, error): + """Show kernel initialization errors in infowidget""" + # Don't break lines in hyphens + # From http://stackoverflow.com/q/7691569/438386 + error = error.replace('-', '‑') + + # Create error page + message = _("An error ocurred while starting the kernel") + kernel_error_template = Template(KERNEL_ERROR) + page = kernel_error_template.substitute(css_path=CSS_PATH, + message=message, + error=error) + + # Show error + self.infowidget.setHtml(page) + self.shellwidget.hide() + self.infowidget.show() + + def get_name(self): + """Return client name""" + return ((_("Console") if self.hostname is None else self.hostname) + + " " + self.name) + + def get_control(self): + """Return the text widget (or similar) to give focus to""" + # page_control is the widget used for paging + page_control = self.shellwidget._page_control + if page_control and page_control.isVisible(): + return page_control + else: + return self.shellwidget._control + + def get_kernel(self): + """Get kernel associated with this client""" + return self.shellwidget.kernel_manager + + def get_options_menu(self): + """Return options menu""" + restart_action = create_action(self, _("Restart kernel"), + shortcut=QKeySequence("Ctrl+."), + icon=ima.icon('restart'), + triggered=self.restart_kernel, + context=Qt.WidgetWithChildrenShortcut) + + # Main menu + if self.menu_actions is not None: + actions = [restart_action, None] + self.menu_actions + else: + actions = [restart_action] + return actions + + def get_toolbar_buttons(self): + """Return toolbar buttons list""" + buttons = [] + # Code to add the stop button + if self.stop_button is None: + self.stop_button = create_toolbutton(self, text=_("Stop"), + icon=self.stop_icon, + tip=_("Stop the current command")) + self.disable_stop_button() + # set click event handler + self.stop_button.clicked.connect(self.stop_button_click_handler) + if self.stop_button is not None: + buttons.append(self.stop_button) + + if self.options_button is None: + options = self.get_options_menu() + if options: + self.options_button = create_toolbutton(self, + text=_('Options'), icon=ima.icon('tooloptions')) + self.options_button.setPopupMode(QToolButton.InstantPopup) + menu = QMenu(self) + add_actions(menu, options) + self.options_button.setMenu(menu) + if self.options_button is not None: + buttons.append(self.options_button) + + return buttons + + def add_actions_to_context_menu(self, menu): + """Add actions to IPython widget context menu""" + inspect_action = create_action(self, _("Inspect current object"), + QKeySequence(get_shortcut('console', + 'inspect current object')), + icon=ima.icon('MessageBoxInformation'), + triggered=self.inspect_object) + clear_line_action = create_action(self, _("Clear line or block"), + QKeySequence("Shift+Escape"), + icon=ima.icon('editdelete'), + triggered=self.clear_line) + reset_namespace_action = create_action(self, _("Reset namespace"), + QKeySequence("Ctrl+Alt+R"), + triggered=self.reset_namespace) + clear_console_action = create_action(self, _("Clear console"), + QKeySequence(get_shortcut('console', + 'clear shell')), + icon=ima.icon('editclear'), + triggered=self.clear_console) + quit_action = create_action(self, _("&Quit"), icon=ima.icon('exit'), + triggered=self.exit_callback) + add_actions(menu, (None, inspect_action, clear_line_action, + clear_console_action, reset_namespace_action, + None, quit_action)) + return menu + + def set_font(self, font): + """Set IPython widget's font""" + self.shellwidget._control.setFont(font) + self.shellwidget.font = font + + def set_infowidget_font(self): + """Set font for infowidget""" + font = get_font(option='rich_font') + self.infowidget.set_font(font) + + def shutdown(self): + """Shutdown kernel""" + if self.get_kernel() is not None and not self.slave: + self.shellwidget.kernel_manager.shutdown_kernel() + if self.shellwidget.kernel_client is not None: + background(self.shellwidget.kernel_client.stop_channels) + + def interrupt_kernel(self): + """Interrupt the associanted Spyder kernel if it's running""" + self.shellwidget.request_interrupt_kernel() + + @Slot() + def restart_kernel(self): + """ + Restart the associanted kernel + + Took this code from the qtconsole project + Licensed under the BSD license + """ + message = _('Are you sure you want to restart the kernel?') + buttons = QMessageBox.Yes | QMessageBox.No + result = QMessageBox.question(self, _('Restart kernel?'), + message, buttons) + if result == QMessageBox.Yes: + sw = self.shellwidget + if sw.kernel_manager: + try: + sw.kernel_manager.restart_kernel() + except RuntimeError as e: + sw._append_plain_text( + _('Error restarting kernel: %s\n') % e, + before_prompt=True + ) + else: + sw._append_html(_("
    Restarting kernel...\n

    "), + before_prompt=True, + ) + else: + sw._append_plain_text( + _('Cannot restart a kernel not started by Spyder\n'), + before_prompt=True + ) + + @Slot() + def inspect_object(self): + """Show how to inspect an object with our Help plugin""" + self.shellwidget._control.inspect_current_object() + + @Slot() + def clear_line(self): + """Clear a console line""" + self.shellwidget._keyboard_quit() + + @Slot() + def clear_console(self): + """Clear the whole console""" + self.shellwidget.clear_console() + + @Slot() + def reset_namespace(self): + """Resets the namespace by removing all names defined by the user""" + self.shellwidget.reset_namespace() + + def update_history(self): + self.history = self.shellwidget._history + + def set_backend_for_mayavi(self, command): + """ + Mayavi plots require the Qt backend, so we try to detect if one is + generated to change backends + """ + calling_mayavi = False + lines = command.splitlines() + for l in lines: + if not l.startswith('#'): + if 'import mayavi' in l or 'from mayavi' in l: + calling_mayavi = True + break + if calling_mayavi: + message = _("Changing backend to Qt for Mayavi") + self.shellwidget._append_plain_text(message + '\n') + self.shellwidget.execute("%gui inline\n%gui qt") + + #------ Private API ------------------------------------------------------- + def _create_loading_page(self): + """Create html page to show while the kernel is starting""" + loading_template = Template(LOADING) + loading_img = get_image_path('loading_sprites.png') + if os.name == 'nt': + loading_img = loading_img.replace('\\', '/') + message = _("Connecting to kernel...") + page = loading_template.substitute(css_path=CSS_PATH, + loading_img=loading_img, + message=message) + return page + + def _stop_loading_animation(self): + """Stop animation shown while the kernel is starting""" + self.infowidget.hide() + self.shellwidget.show() + self.infowidget.setHtml(BLANK) + + document = self.get_control().document() + document.contentsChange.disconnect(self._stop_loading_animation) diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/widgets/ipythonconsole/control.py spyder-3.0.2+dfsg1/spyder/widgets/ipythonconsole/control.py --- spyder-2.3.8+dfsg1/spyder/widgets/ipythonconsole/control.py 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/widgets/ipythonconsole/control.py 2016-11-16 16:40:01.000000000 +0100 @@ -0,0 +1,109 @@ +# -*- coding: utf-8 -*- +# +# Copyright © Spyder Project Contributors +# Licensed under the terms of the MIT License +# (see spyder/__init__.py for details) + +"""Control widgets used by ShellWidget""" + +from qtpy.QtCore import Qt, Signal +from qtpy.QtWidgets import QTextEdit + +from spyder.utils.qthelpers import restore_keyevent +from spyder.widgets.calltip import CallTipWidget +from spyder.widgets.mixins import (BaseEditMixin, GetHelpMixin, + TracebackLinksMixin) + + +class ControlWidget(TracebackLinksMixin, GetHelpMixin, QTextEdit, + BaseEditMixin): + """ + Subclass of QTextEdit with features from Spyder's mixins to use as the + control widget for IPython widgets + """ + QT_CLASS = QTextEdit + visibility_changed = Signal(bool) + go_to_error = Signal(str) + focus_changed = Signal() + + def __init__(self, parent=None): + QTextEdit.__init__(self, parent) + BaseEditMixin.__init__(self) + TracebackLinksMixin.__init__(self) + GetHelpMixin.__init__(self) + + self.calltip_widget = CallTipWidget(self, hide_timer_on=True) + self.found_results = [] + + # To not use Spyder calltips obtained through the monitor + self.calltips = False + + def showEvent(self, event): + """Reimplement Qt Method""" + self.visibility_changed.emit(True) + + def _key_paren_left(self, text): + """ Action for '(' """ + self.current_prompt_pos = self.parentWidget()._prompt_pos + if self.get_current_line_to_cursor(): + last_obj = self.get_last_obj() + if last_obj and not last_obj.isdigit(): + self.show_object_info(last_obj) + self.insert_text(text) + + def keyPressEvent(self, event): + """Reimplement Qt Method - Basic keypress event handler""" + event, text, key, ctrl, shift = restore_keyevent(event) + if key == Qt.Key_ParenLeft and not self.has_selected_text() \ + and self.help_enabled and not self.parent()._reading: + self._key_paren_left(text) + else: + # Let the parent widget handle the key press event + QTextEdit.keyPressEvent(self, event) + + def focusInEvent(self, event): + """Reimplement Qt method to send focus change notification""" + self.focus_changed.emit() + return super(ControlWidget, self).focusInEvent(event) + + def focusOutEvent(self, event): + """Reimplement Qt method to send focus change notification""" + self.focus_changed.emit() + return super(ControlWidget, self).focusOutEvent(event) + + +class PageControlWidget(QTextEdit, BaseEditMixin): + """ + Subclass of QTextEdit with features from Spyder's mixins.BaseEditMixin to + use as the paging widget for IPython widgets + """ + QT_CLASS = QTextEdit + visibility_changed = Signal(bool) + show_find_widget = Signal() + focus_changed = Signal() + + def __init__(self, parent=None): + QTextEdit.__init__(self, parent) + BaseEditMixin.__init__(self) + self.found_results = [] + + def showEvent(self, event): + """Reimplement Qt Method""" + self.visibility_changed.emit(True) + + def keyPressEvent(self, event): + """Reimplement Qt Method - Basic keypress event handler""" + event, text, key, ctrl, shift = restore_keyevent(event) + + if key == Qt.Key_Slash and self.isVisible(): + self.show_find_widget.emit() + + def focusInEvent(self, event): + """Reimplement Qt method to send focus change notification""" + self.focus_changed.emit() + return super(PageControlWidget, self).focusInEvent(event) + + def focusOutEvent(self, event): + """Reimplement Qt method to send focus change notification""" + self.focus_changed.emit() + return super(PageControlWidget, self).focusOutEvent(event) diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/widgets/ipythonconsole/debugging.py spyder-3.0.2+dfsg1/spyder/widgets/ipythonconsole/debugging.py --- spyder-2.3.8+dfsg1/spyder/widgets/ipythonconsole/debugging.py 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/widgets/ipythonconsole/debugging.py 2016-11-16 16:40:01.000000000 +0100 @@ -0,0 +1,140 @@ +# -*- coding: utf-8 -*- +# +# Copyright © Spyder Project Contributors +# Licensed under the terms of the MIT License +# (see spyder/__init__.py for details) + +""" +Widget that handles communications between a console in debugging +mode and Spyder +""" + +import ast + +from qtpy.QtCore import QEventLoop + +from qtconsole.rich_jupyter_widget import RichJupyterWidget + +from spyder.py3compat import to_text_string + + +class DebuggingWidget(RichJupyterWidget): + """ + Widget with the necessary attributes and methods to handle + communications between a console in debugging mode and + Spyder + """ + + _input_reply = None + + # --- Public API -------------------------------------------------- + def silent_exec_input(self, code): + """Silently execute code through stdin""" + self._hidden = True + + # Wait until the kernel returns an answer + wait_loop = QEventLoop() + self.sig_input_reply.connect(wait_loop.quit) + self.kernel_client.iopub_channel.flush() + self.kernel_client.input(code) + wait_loop.exec_() + + # Remove loop connection and loop + self.sig_input_reply.disconnect(wait_loop.quit) + wait_loop = None + + # Restore hidden state + self._hidden = False + + # Emit signal + if 'pdb_step' in code and self._input_reply is not None: + fname = self._input_reply['fname'] + lineno = self._input_reply['lineno'] + self.sig_pdb_step.emit(fname, lineno) + elif 'get_namespace_view' in code: + view = self._input_reply + self.sig_namespace_view.emit(view) + elif 'get_var_properties' in code: + properties = self._input_reply + self.sig_var_properties.emit(properties) + + def write_to_stdin(self, line): + """Send raw characters to the IPython kernel through stdin""" + wait_loop = QEventLoop() + self.sig_prompt_ready.connect(wait_loop.quit) + self.kernel_client.input(line) + wait_loop.exec_() + + # Remove loop connection and loop + self.sig_prompt_ready.disconnect(wait_loop.quit) + wait_loop = None + + # Run post exec commands + self._post_exec_input(line) + + # ---- Private API (defined by us) ------------------------------- + def _post_exec_input(self, line): + """Commands to be run after writing to stdin""" + if self._reading: + pdb_commands = ['next', 'continue', 'step', 'return'] + if any([x == line for x in pdb_commands]): + # To open the file where the current pdb frame points to + self.silent_exec_input("!get_ipython().kernel.get_pdb_step()") + + # To refresh the Variable Explorer + self.silent_exec_input( + "!get_ipython().kernel.get_namespace_view()") + self.silent_exec_input( + "!get_ipython().kernel.get_var_properties()") + + # ---- Private API (overrode by us) ------------------------------- + def _handle_input_request(self, msg): + """ + Reimplemented to be able to handle requests when we ask for + hidden inputs + """ + self.kernel_client.iopub_channel.flush() + if not self._hidden: + def callback(line): + self.kernel_client.input(line) + if self._reading: + self._reading = False + self._readline(msg['content']['prompt'], callback=callback, + password=msg['content']['password']) + else: + # This is what we added, i.e. not doing anything if + # Spyder asks for silent inputs + pass + + def _handle_stream(self, msg): + """ + Reimplemented to handle input replies in hidden mode + """ + if not self._hidden: + self.flush_clearoutput() + self.append_stream(msg['content']['text']) + # This signal is a clear indication that all stdout + # has been handled at this point. Then Spyder can + # proceed to request other inputs + self.sig_prompt_ready.emit() + else: + # This allows Spyder to receive, transform and save the + # contents of a silent execution + content = msg.get('content', '') + if content: + name = content.get('name', '') + if name == 'stdout': + text = content['text'] + text = to_text_string(text.replace('\n', '')) + try: + reply = ast.literal_eval(text) + except: + reply = None + self._input_reply = reply + self.sig_input_reply.emit() + else: + self._input_reply = None + self.sig_input_reply.emit() + else: + self._input_reply = None + self.sig_input_reply.emit() diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/widgets/ipythonconsole/help.py spyder-3.0.2+dfsg1/spyder/widgets/ipythonconsole/help.py --- spyder-2.3.8+dfsg1/spyder/widgets/ipythonconsole/help.py 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/widgets/ipythonconsole/help.py 2016-10-25 02:05:23.000000000 +0200 @@ -0,0 +1,126 @@ +# -*- coding: utf-8 -*- +# +# Copyright © Spyder Project Contributors +# Licensed under the terms of the MIT License +# (see spyder/__init__.py for details) + +""" +Widget that handles communications between the IPython Console and +the Help plugin +""" + +from __future__ import absolute_import + +import re + +from qtpy.QtCore import QEventLoop + +from qtconsole.ansi_code_processor import ANSI_OR_SPECIAL_PATTERN +from qtconsole.rich_jupyter_widget import RichJupyterWidget + +from spyder.config.base import _ +from spyder.py3compat import PY3 +from spyder.utils.dochelpers import getargspecfromtext, getsignaturefromtext + + +class HelpWidget(RichJupyterWidget): + """ + Widget with the necessary attributes and methods to handle communications + between the IPython Console and the Help plugin + """ + + def clean_invalid_var_chars(self, var): + """ + Replace invalid variable chars in a string by underscores + + Taken from http://stackoverflow.com/a/3305731/438386 + """ + if PY3: + return re.sub('\W|^(?=\d)', '_', var, re.UNICODE) + else: + return re.sub('\W|^(?=\d)', '_', var) + + def get_signature(self, content): + """Get signature from inspect reply content""" + data = content.get('data', {}) + text = data.get('text/plain', '') + if text: + text = ANSI_OR_SPECIAL_PATTERN.sub('', text) + self._control.current_prompt_pos = self._prompt_pos + line = self._control.get_current_line_to_cursor() + name = line[:-1].split('(')[-1] # Take last token after a ( + name = name.split('.')[-1] # Then take last token after a . + # Clean name from invalid chars + try: + name = self.clean_invalid_var_chars(name).split('_')[-1] + except: + pass + argspec = getargspecfromtext(text) + if argspec: + # This covers cases like np.abs, whose docstring is + # the same as np.absolute and because of that a proper + # signature can't be obtained correctly + signature = name + argspec + else: + signature = getsignaturefromtext(text, name) + return signature + else: + return '' + + def is_defined(self, objtxt, force_import=False): + """Return True if object is defined""" + wait_loop = QEventLoop() + self.sig_got_reply.connect(wait_loop.quit) + self.silent_exec_method( + "get_ipython().kernel.is_defined('%s', force_import=%s)" + % (objtxt, force_import)) + wait_loop.exec_() + + # Remove loop connection and loop + self.sig_got_reply.disconnect(wait_loop.quit) + wait_loop = None + + return self._kernel_reply + + def get_doc(self, objtxt): + """Get object documentation dictionary""" + wait_loop = QEventLoop() + self.sig_got_reply.connect(wait_loop.quit) + self.silent_exec_method("get_ipython().kernel.get_doc('%s')" % objtxt) + wait_loop.exec_() + + # Remove loop connection and loop + self.sig_got_reply.disconnect(wait_loop.quit) + wait_loop = None + + return self._kernel_reply + + def get_source(self, objtxt): + """Get object source""" + wait_loop = QEventLoop() + self.sig_got_reply.connect(wait_loop.quit) + self.silent_exec_method("get_ipython().kernel.get_source('%s')" % objtxt) + wait_loop.exec_() + + # Remove loop connection and loop + self.sig_got_reply.disconnect(wait_loop.quit) + wait_loop = None + + return self._kernel_reply + + #---- Private methods (overrode by us) --------------------------------- + def _handle_inspect_reply(self, rep): + """ + Reimplement call tips to only show signatures, using the same + style from our Editor and External Console too + """ + cursor = self._get_cursor() + info = self._request_info.get('call_tip') + if info and info.id == rep['parent_header']['msg_id'] and \ + info.pos == cursor.position(): + content = rep['content'] + if content.get('status') == 'ok' and content.get('found', False): + signature = self.get_signature(content) + if signature: + self._control.show_calltip(_("Arguments"), signature, + signature=True, color='#2D62FF') diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/widgets/ipythonconsole/__init__.py spyder-3.0.2+dfsg1/spyder/widgets/ipythonconsole/__init__.py --- spyder-2.3.8+dfsg1/spyder/widgets/ipythonconsole/__init__.py 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/widgets/ipythonconsole/__init__.py 2016-10-25 02:05:23.000000000 +0200 @@ -0,0 +1,19 @@ +# -*- coding:utf-8 -*- +# +# Copyright © Spyder Project Contributors +# Licensed under the terms of the MIT License +# (see spyder/__init__.py for details) + +""" +Widgets for the IPython Console +""" + +from .control import ControlWidget, PageControlWidget +from .debugging import DebuggingWidget +from .help import HelpWidget +from .namespacebrowser import NamepaceBrowserWidget + +# ShellWidget contains the other widgets and ClientWidget +# contains it +from .shell import ShellWidget +from .client import ClientWidget diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/widgets/ipythonconsole/namespacebrowser.py spyder-3.0.2+dfsg1/spyder/widgets/ipythonconsole/namespacebrowser.py --- spyder-2.3.8+dfsg1/spyder/widgets/ipythonconsole/namespacebrowser.py 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/widgets/ipythonconsole/namespacebrowser.py 2016-11-17 04:39:40.000000000 +0100 @@ -0,0 +1,211 @@ +# -*- coding: utf-8 -*- +# +# Copyright © Spyder Project Contributors +# Licensed under the terms of the MIT License +# (see spyder/__init__.py for details) + +""" +Widget that handle communications between the IPython Console and +the Variable Explorer +""" + +from qtpy.QtCore import QEventLoop + +from ipykernel.pickleutil import CannedObject +from ipykernel.serialize import deserialize_object +from qtconsole.rich_jupyter_widget import RichJupyterWidget + +from spyder.config.base import _ +from spyder.py3compat import to_text_string + + +class NamepaceBrowserWidget(RichJupyterWidget): + """ + Widget with the necessary attributes and methods to handle communications + between the IPython Console and the Variable Explorer + """ + + # Reference to the nsb widget connected to this client + namespacebrowser = None + + # To save the replies of kernel method executions (except + # getting values of variables) + _kernel_methods = {} + + # To save values and messages returned by the kernel + _kernel_value = None + _kernel_is_starting = True + + # --- Public API -------------------------------------------------- + def set_namespacebrowser(self, namespacebrowser): + """Set namespace browser widget""" + self.namespacebrowser = namespacebrowser + self.configure_namespacebrowser() + + def configure_namespacebrowser(self): + """Configure associated namespace browser widget""" + # Tell it that we are connected to client + self.namespacebrowser.is_ipyclient = True + + # Update namespace view + self.sig_namespace_view.connect(lambda data: + self.namespacebrowser.process_remote_view(data)) + + # Update properties of variables + self.sig_var_properties.connect(lambda data: + self.namespacebrowser.set_var_properties(data)) + + def refresh_namespacebrowser(self): + """Refresh namespace browser""" + if self.namespacebrowser: + self.silent_exec_method( + 'get_ipython().kernel.get_namespace_view()') + self.silent_exec_method( + 'get_ipython().kernel.get_var_properties()') + + def set_namespace_view_settings(self): + """Set the namespace view settings""" + settings = to_text_string(self.namespacebrowser.get_view_settings()) + code = u"get_ipython().kernel.namespace_view_settings = %s" % settings + self.silent_execute(code) + + def get_value(self, name): + """Ask kernel for a value""" + # Don't ask for values while reading (ipdb) is active + if self._reading: + raise ValueError(_("Inspecting and setting values while debugging " + "in IPython consoles is not supported yet by " + "Spyder.")) + + # Wait until the kernel returns the value + wait_loop = QEventLoop() + self.sig_got_reply.connect(wait_loop.quit) + self.silent_execute("get_ipython().kernel.get_value('%s')" % name) + wait_loop.exec_() + + # Remove loop connection and loop + self.sig_got_reply.disconnect(wait_loop.quit) + wait_loop = None + + # Handle exceptions + if self._kernel_value is None: + if self._kernel_reply: + msg = self._kernel_reply[:] + self._kernel_reply = None + raise ValueError(msg) + + return self._kernel_value + + def set_value(self, name, value): + """Set value for a variable""" + value = to_text_string(value) + self.silent_execute("get_ipython().kernel.set_value('%s', %s)" % + (name, value)) + + def remove_value(self, name): + """Remove a variable""" + self.silent_execute("get_ipython().kernel.remove_value('%s')" % name) + + def copy_value(self, orig_name, new_name): + """Copy a variable""" + self.silent_execute("get_ipython().kernel.copy_value('%s', '%s')" % + (orig_name, new_name)) + + def load_data(self, filename, ext): + # Wait until the kernel tries to load the file + wait_loop = QEventLoop() + self.sig_got_reply.connect(wait_loop.quit) + self.silent_exec_method( + "get_ipython().kernel.load_data('%s', '%s')" % (filename, ext)) + wait_loop.exec_() + + # Remove loop connection and loop + self.sig_got_reply.disconnect(wait_loop.quit) + wait_loop = None + + return self._kernel_reply + + def save_namespace(self, filename): + # 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')" % + filename) + wait_loop.exec_() + + # Remove loop connection and loop + self.sig_got_reply.disconnect(wait_loop.quit) + wait_loop = None + + return self._kernel_reply + + # ---- Private API (defined by us) ------------------------------ + def _handle_data_message(self, msg): + """ + Handle raw (serialized) data sent by the kernel + + We only handle data asked by Spyder, in case people use + publish_data for other purposes. + """ + # Deserialize data + try: + data = deserialize_object(msg['buffers'])[0] + except Exception as msg: + self._kernel_value = None + self._kernel_reply = repr(msg) + self.sig_got_reply.emit() + return + + # We only handle data asked by Spyder + value = data.get('__spy_data__', None) + if value is not None: + if isinstance(value, CannedObject): + value = value.get_object() + self._kernel_value = value + self.sig_got_reply.emit() + + # ---- Private API (overrode by us) ---------------------------- + def _handle_execute_reply(self, msg): + """ + Reimplemented to handle communications between Spyder + and the kernel + """ + msg_id = msg['parent_header']['msg_id'] + info = self._request_info['execute'].get(msg_id) + # unset reading flag, because if execute finished, raw_input can't + # still be pending. + self._reading = False + + # Refresh namespacebrowser after the kernel starts running + exec_count = msg['content']['execution_count'] + if exec_count == 0 and self._kernel_is_starting: + if self.namespacebrowser is not None: + self.set_namespace_view_settings() + self.refresh_namespacebrowser() + self._kernel_is_starting = False + + # Handle silent execution of kernel methods + if info and info.kind == 'silent_exec_method' and not self._hidden: + self.handle_exec_method(msg) + self._request_info['execute'].pop(msg_id) + else: + super(NamepaceBrowserWidget, self)._handle_execute_reply(msg) + + def _handle_status(self, msg): + """ + Reimplemented to refresh the namespacebrowser after kernel + restarts + """ + state = msg['content'].get('execution_state', '') + msg_type = msg['parent_header'].get('msg_type', '') + if state == 'starting' and not self._kernel_is_starting: + # This handles restarts when the kernel dies + # unexpectedly + self._kernel_is_starting = True + elif state == 'idle' and msg_type == 'shutdown_request': + # This handles restarts asked by the user + if self.namespacebrowser is not None: + self.set_namespace_view_settings() + self.refresh_namespacebrowser() + else: + super(NamepaceBrowserWidget, self)._handle_status(msg) diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/widgets/ipythonconsole/shell.py spyder-3.0.2+dfsg1/spyder/widgets/ipythonconsole/shell.py --- spyder-2.3.8+dfsg1/spyder/widgets/ipythonconsole/shell.py 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/widgets/ipythonconsole/shell.py 2016-11-17 04:39:40.000000000 +0100 @@ -0,0 +1,283 @@ +# -*- coding: utf-8 -*- +# +# Copyright © Spyder Project Contributors +# Licensed under the terms of the MIT License +# (see spyder/__init__.py for details) + +""" +Shell Widget for the IPython Console +""" + +import ast +import uuid + +from qtpy.QtCore import Signal +from qtpy.QtWidgets import QMessageBox + +from spyder.config.base import _ +from spyder.config.gui import config_shortcut, fixed_shortcut +from spyder.py3compat import to_text_string +from spyder.utils import programs +from spyder.widgets.arraybuilder import SHORTCUT_INLINE, SHORTCUT_TABLE +from spyder.widgets.ipythonconsole import (ControlWidget, DebuggingWidget, + HelpWidget, NamepaceBrowserWidget, + PageControlWidget) + + +class ShellWidget(NamepaceBrowserWidget, HelpWidget, DebuggingWidget): + """ + Shell widget for the IPython Console + + This is the widget in charge of executing code + """ + # NOTE: Signals can't be assigned separately to each widget + # That's why we define all needed signals here. + + # For NamepaceBrowserWidget + sig_namespace_view = Signal(object) + sig_var_properties = Signal(object) + + # For DebuggingWidget + sig_input_reply = Signal() + sig_pdb_step = Signal(str, int) + sig_prompt_ready = Signal() + + # For ShellWidget + focus_changed = Signal() + new_client = Signal() + sig_got_reply = Signal() + + def __init__(self, additional_options, interpreter_versions, + external_kernel, *args, **kw): + # To override the Qt widget used by RichJupyterWidget + self.custom_control = ControlWidget + self.custom_page_control = PageControlWidget + super(ShellWidget, self).__init__(*args, **kw) + self.additional_options = additional_options + self.interpreter_versions = interpreter_versions + + self.set_background_color() + + # Additional variables + self.ipyclient = None + self.external_kernel = external_kernel + + # Keyboard shortcuts + self.shortcuts = self.create_shortcuts() + + # To save kernel replies in silent execution + self._kernel_reply = None + + #---- Public API ---------------------------------------------------------- + def set_ipyclient(self, ipyclient): + """Bind this shell widget to an IPython client one""" + self.ipyclient = ipyclient + self.exit_requested.connect(ipyclient.exit_callback) + + def is_running(self): + if self.kernel_client is not None and \ + self.kernel_client.channels_running: + return True + else: + return False + + def set_cwd(self, dirname): + """Set shell current working directory.""" + return self.silent_execute( + "get_ipython().kernel.set_cwd(r'{}')".format(dirname)) + + # --- To handle the banner + def long_banner(self): + """Banner for IPython widgets with pylab message""" + # Default banner + from IPython.core.usage import quick_guide + banner_parts = [ + 'Python %s\n' % self.interpreter_versions['python_version'], + 'Type "copyright", "credits" or "license" for more information.\n\n', + 'IPython %s -- An enhanced Interactive Python.\n' % \ + self.interpreter_versions['ipython_version'], + quick_guide + ] + banner = ''.join(banner_parts) + + # Pylab additions + pylab_o = self.additional_options['pylab'] + autoload_pylab_o = self.additional_options['autoload_pylab'] + mpl_installed = programs.is_module_installed('matplotlib') + if mpl_installed and (pylab_o and autoload_pylab_o): + pylab_message = ("\nPopulating the interactive namespace from " + "numpy and matplotlib\n") + banner = banner + pylab_message + + # Sympy additions + sympy_o = self.additional_options['sympy'] + if sympy_o: + lines = """ +These commands were executed: +>>> from __future__ import division +>>> from sympy import * +>>> x, y, z, t = symbols('x y z t') +>>> k, m, n = symbols('k m n', integer=True) +>>> f, g, h = symbols('f g h', cls=Function) +""" + banner = banner + lines + if (pylab_o and sympy_o): + lines = """ +Warning: pylab (numpy and matplotlib) and symbolic math (sympy) are both +enabled at the same time. Some pylab functions are going to be overrided by +the sympy module (e.g. plot) +""" + banner = banner + lines + return banner + + def short_banner(self): + """Short banner with Python and QtConsole versions""" + banner = 'Python %s -- IPython %s' % ( + self.interpreter_versions['python_version'], + self.interpreter_versions['ipython_version']) + return banner + + # --- To define additional shortcuts + def clear_console(self): + self.execute("%clear") + + def reset_namespace(self): + """Resets the namespace by removing all names defined by the user""" + + reply = QMessageBox.question( + self, + _("Reset IPython namespace"), + _("All user-defined variables will be removed." + "
    Are you sure you want to reset the namespace?"), + QMessageBox.Yes | QMessageBox.No, + ) + + if reply == QMessageBox.Yes: + self.execute("%reset -f") + + def set_background_color(self): + light_color_o = self.additional_options['light_color'] + if not light_color_o: + self.set_default_style(colors='linux') + + def create_shortcuts(self): + inspect = config_shortcut(self._control.inspect_current_object, + context='Console', name='Inspect current object', + parent=self) + clear_console = config_shortcut(self.clear_console, context='Console', + name='Clear shell', parent=self) + + # Fixed shortcuts + fixed_shortcut("Ctrl+T", self, lambda: self.new_client.emit()) + fixed_shortcut("Ctrl+Alt+R", self, lambda: self.reset_namespace()) + fixed_shortcut(SHORTCUT_INLINE, self, + lambda: self._control.enter_array_inline()) + fixed_shortcut(SHORTCUT_TABLE, self, + lambda: self._control.enter_array_table()) + + return [inspect, clear_console] + + # --- To communicate with the kernel + def silent_execute(self, code): + """Execute code in the kernel without increasing the prompt""" + self.kernel_client.execute(to_text_string(code), silent=True) + + def silent_exec_method(self, code): + """Silently execute a kernel method and save its reply + + The methods passed here **don't** involve getting the value + of a variable but instead replies that can be handled by + ast.literal_eval. + + To get a value see `get_value` + + Parameters + ---------- + code : string + Code that contains the kernel method as part of its + string + + See Also + -------- + handle_exec_method : Method that deals with the reply + + Note + ---- + This is based on the _silent_exec_callback method of + RichJupyterWidget. Therefore this is licensed BSD + """ + # Generate uuid, which would be used as an indication of whether or + # not the unique request originated from here + local_uuid = to_text_string(uuid.uuid1()) + code = to_text_string(code) + msg_id = self.kernel_client.execute('', silent=True, + user_expressions={ local_uuid:code }) + self._kernel_methods[local_uuid] = code + self._request_info['execute'][msg_id] = self._ExecutionRequest(msg_id, + 'silent_exec_method') + + def handle_exec_method(self, msg): + """ + Handle data returned by silent executions of kernel methods + + This is based on the _handle_exec_callback of RichJupyterWidget. + Therefore this is licensed BSD. + """ + user_exp = msg['content'].get('user_expressions') + if not user_exp: + return + for expression in user_exp: + if expression in self._kernel_methods: + # Process kernel reply + method = self._kernel_methods[expression] + reply = user_exp[expression] + data = reply.get('data') + if 'get_namespace_view' in method: + if data is not None and 'text/plain' in data: + view = ast.literal_eval(data['text/plain']) + self.sig_namespace_view.emit(view) + else: + view = None + elif 'get_var_properties' in method: + properties = ast.literal_eval(data['text/plain']) + self.sig_var_properties.emit(properties) + else: + if data is not None: + self._kernel_reply = ast.literal_eval(data['text/plain']) + else: + self._kernel_reply = None + self.sig_got_reply.emit() + + # Remove method after being processed + self._kernel_methods.pop(expression) + + #---- Private methods (overrode by us) --------------------------------- + def _context_menu_make(self, pos): + """Reimplement the IPython context menu""" + menu = super(ShellWidget, self)._context_menu_make(pos) + return self.ipyclient.add_actions_to_context_menu(menu) + + def _banner_default(self): + """ + Reimplement banner creation to let the user decide if he wants a + banner or not + """ + # Don't change banner for external kernels + if self.external_kernel: + return '' + show_banner_o = self.additional_options['show_banner'] + if show_banner_o: + return self.long_banner() + else: + return self.short_banner() + + #---- Qt methods ---------------------------------------------------------- + def focusInEvent(self, event): + """Reimplement Qt method to send focus change notification""" + self.focus_changed.emit() + return super(ShellWidget, self).focusInEvent(event) + + def focusOutEvent(self, event): + """Reimplement Qt method to send focus change notification""" + self.focus_changed.emit() + return super(ShellWidget, self).focusOutEvent(event) diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/widgets/ipython.py spyder-3.0.2+dfsg1/spyder/widgets/ipython.py --- spyder-2.3.8+dfsg1/spyder/widgets/ipython.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/widgets/ipython.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,748 +0,0 @@ -# -*- coding:utf-8 -*- -# -# Copyright © 2011-2012 Pierre Raybaut -# Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) - -""" -IPython v0.13+ client's widget -""" -# Fix for Issue 1356 -from __future__ import absolute_import - -# Stdlib imports -import os -import os.path as osp -import re -from string import Template -import sys -import time - -# Qt imports -from spyderlib.qt.QtGui import (QTextEdit, QKeySequence, QWidget, QMenu, - QHBoxLayout, QToolButton, QVBoxLayout, - QMessageBox) -from spyderlib.qt.QtCore import SIGNAL, Qt - -from spyderlib import pygments_patch -pygments_patch.apply() - -# IPython imports -try: - from qtconsole.rich_jupyter_widget import RichJupyterWidget as RichIPythonWidget -except ImportError: - from IPython.qt.console.rich_ipython_widget import RichIPythonWidget -from IPython.qt.console.ansi_code_processor import ANSI_OR_SPECIAL_PATTERN -from IPython.core.application import get_ipython_dir -from IPython.core.oinspect import call_tip -from IPython.config.loader import Config, load_pyconfig_files - -# Local imports -from spyderlib.baseconfig import (get_conf_path, get_image_path, - get_module_source_path, _) -from spyderlib.config import CONF -from spyderlib.guiconfig import (create_shortcut, get_font, get_shortcut, - new_shortcut) -from spyderlib.utils.dochelpers import getargspecfromtext, getsignaturefromtext -from spyderlib.utils.qthelpers import (get_std_icon, create_toolbutton, - add_actions, create_action, get_icon, - restore_keyevent) -from spyderlib.utils import programs, sourcecode -from spyderlib.widgets.browser import WebView -from spyderlib.widgets.calltip import CallTipWidget -from spyderlib.widgets.mixins import (BaseEditMixin, InspectObjectMixin, - SaveHistoryMixin, TracebackLinksMixin) -from spyderlib.py3compat import PY3 - - -#----------------------------------------------------------------------------- -# Templates -#----------------------------------------------------------------------------- -# Using the same css file from the Object Inspector for now. Maybe -# later it'll be a good idea to create a new one. -UTILS_PATH = get_module_source_path('spyderlib', 'utils') -CSS_PATH = osp.join(UTILS_PATH, 'inspector', 'static', 'css') -TEMPLATES_PATH = osp.join(UTILS_PATH, 'ipython', 'templates') - -BLANK = open(osp.join(TEMPLATES_PATH, 'blank.html')).read() -LOADING = open(osp.join(TEMPLATES_PATH, 'loading.html')).read() -KERNEL_ERROR = open(osp.join(TEMPLATES_PATH, 'kernel_error.html')).read() - -#----------------------------------------------------------------------------- -# Control widgets -#----------------------------------------------------------------------------- -class IPythonControlWidget(TracebackLinksMixin, InspectObjectMixin, QTextEdit, - BaseEditMixin): - """ - Subclass of QTextEdit with features from Spyder's mixins to use as the - control widget for IPython widgets - """ - QT_CLASS = QTextEdit - def __init__(self, parent=None): - QTextEdit.__init__(self, parent) - BaseEditMixin.__init__(self) - TracebackLinksMixin.__init__(self) - InspectObjectMixin.__init__(self) - - self.calltip_widget = CallTipWidget(self, hide_timer_on=True) - self.found_results = [] - - # To not use Spyder calltips obtained through the monitor - self.calltips = False - - def showEvent(self, event): - """Reimplement Qt Method""" - self.emit(SIGNAL("visibility_changed(bool)"), True) - - def _key_question(self, text): - """ Action for '?' and '(' """ - self.current_prompt_pos = self.parentWidget()._prompt_pos - if self.get_current_line_to_cursor(): - last_obj = self.get_last_obj() - if last_obj and not last_obj.isdigit(): - self.show_object_info(last_obj) - self.insert_text(text) - - def keyPressEvent(self, event): - """Reimplement Qt Method - Basic keypress event handler""" - event, text, key, ctrl, shift = restore_keyevent(event) - if key == Qt.Key_Question and not self.has_selected_text(): - self._key_question(text) - elif key == Qt.Key_ParenLeft and not self.has_selected_text(): - self._key_question(text) - else: - # Let the parent widget handle the key press event - QTextEdit.keyPressEvent(self, event) - - def focusInEvent(self, event): - """Reimplement Qt method to send focus change notification""" - self.emit(SIGNAL('focus_changed()')) - return super(IPythonControlWidget, self).focusInEvent(event) - - def focusOutEvent(self, event): - """Reimplement Qt method to send focus change notification""" - self.emit(SIGNAL('focus_changed()')) - return super(IPythonControlWidget, self).focusOutEvent(event) - - -class IPythonPageControlWidget(QTextEdit, BaseEditMixin): - """ - Subclass of QTextEdit with features from Spyder's mixins.BaseEditMixin to - use as the paging widget for IPython widgets - """ - QT_CLASS = QTextEdit - def __init__(self, parent=None): - QTextEdit.__init__(self, parent) - BaseEditMixin.__init__(self) - self.found_results = [] - - def showEvent(self, event): - """Reimplement Qt Method""" - self.emit(SIGNAL("visibility_changed(bool)"), True) - - def keyPressEvent(self, event): - """Reimplement Qt Method - Basic keypress event handler""" - event, text, key, ctrl, shift = restore_keyevent(event) - - if key == Qt.Key_Slash and self.isVisible(): - self.emit(SIGNAL("show_find_widget()")) - - def focusInEvent(self, event): - """Reimplement Qt method to send focus change notification""" - self.emit(SIGNAL('focus_changed()')) - return super(IPythonPageControlWidget, self).focusInEvent(event) - - def focusOutEvent(self, event): - """Reimplement Qt method to send focus change notification""" - self.emit(SIGNAL('focus_changed()')) - return super(IPythonPageControlWidget, self).focusOutEvent(event) - - -#----------------------------------------------------------------------------- -# Shell widget -#----------------------------------------------------------------------------- -class IPythonShellWidget(RichIPythonWidget): - """ - Spyder's IPython shell widget - - This class has custom control and page_control widgets, additional methods - to provide missing functionality and a couple more keyboard shortcuts. - """ - def __init__(self, *args, **kw): - # To override the Qt widget used by RichIPythonWidget - self.custom_control = IPythonControlWidget - self.custom_page_control = IPythonPageControlWidget - super(IPythonShellWidget, self).__init__(*args, **kw) - self.set_background_color() - - # --- Spyder variables --- - self.ipyclient = None - - # --- Keyboard shortcuts --- - self.shortcuts = self.create_shortcuts() - - # --- IPython variables --- - # To send an interrupt signal to the Spyder kernel - self.custom_interrupt = True - - # To restart the Spyder kernel in case it dies - self.custom_restart = True - - #---- Public API ---------------------------------------------------------- - def set_ipyclient(self, ipyclient): - """Bind this shell widget to an IPython client one""" - self.ipyclient = ipyclient - self.exit_requested.connect(ipyclient.exit_callback) - - def long_banner(self): - """Banner for IPython widgets with pylab message""" - from IPython.core.usage import default_gui_banner - banner = default_gui_banner - - pylab_o = CONF.get('ipython_console', 'pylab', True) - autoload_pylab_o = CONF.get('ipython_console', 'pylab/autoload', True) - mpl_installed = programs.is_module_installed('matplotlib') - if mpl_installed and (pylab_o and autoload_pylab_o): - pylab_message = ("\nPopulating the interactive namespace from " - "numpy and matplotlib") - banner = banner + pylab_message - - sympy_o = CONF.get('ipython_console', 'symbolic_math', True) - if sympy_o: - lines = """ -These commands were executed: ->>> from __future__ import division ->>> from sympy import * ->>> x, y, z, t = symbols('x y z t') ->>> k, m, n = symbols('k m n', integer=True) ->>> f, g, h = symbols('f g h', cls=Function) -""" - banner = banner + lines - return banner - - def short_banner(self): - """Short banner with Python and IPython versions""" - from IPython.core.release import version - py_ver = '%d.%d.%d' % (sys.version_info[0], sys.version_info[1], - sys.version_info[2]) - banner = 'Python %s on %s -- IPython %s' % (py_ver, sys.platform, - version) - return banner - - def clear_console(self): - self.execute("%clear") - - def write_to_stdin(self, line): - """Send raw characters to the IPython kernel through stdin""" - try: - self.kernel_client.stdin_channel.input(line) - except AttributeError: - self.kernel_client.input(line) - - def set_background_color(self): - lightbg_o = CONF.get('ipython_console', 'light_color') - if not lightbg_o: - self.set_default_style(colors='linux') - - def create_shortcuts(self): - inspect = create_shortcut(self._control.inspect_current_object, - context='Console', name='Inspect current object', - parent=self) - clear_console = create_shortcut(self.clear_console, context='Console', - name='Clear shell', parent=self) - - # Fixed shortcuts - new_shortcut("Ctrl+T", self, - lambda: self.emit(SIGNAL("new_ipyclient()"))) - - return [inspect, clear_console] - - def clean_invalid_var_chars(self, var): - """ - Replace invalid variable chars in a string by underscores - - Taken from http://stackoverflow.com/a/3305731/438386 - """ - if PY3: - return re.sub('\W|^(?=\d)', '_', var, re.UNICODE) - else: - return re.sub('\W|^(?=\d)', '_', var) - - def get_signature(self, content): - """Get signature from inspect reply content""" - data = content.get('data', {}) - text = data.get('text/plain', '') - if text: - text = ANSI_OR_SPECIAL_PATTERN.sub('', text) - self._control.current_prompt_pos = self._prompt_pos - line = self._control.get_current_line_to_cursor() - name = line[:-1].split('(')[-1] # Take last token after a ( - name = name.split('.')[-1] # Then take last token after a . - # Clean name from invalid chars - try: - name = self.clean_invalid_var_chars(name).split('_')[-1] - except: - pass - argspec = getargspecfromtext(text) - if argspec: - # This covers cases like np.abs, whose docstring is - # the same as np.absolute and because of that a proper - # signature can't be obtained correctly - signature = name + argspec - else: - signature = getsignaturefromtext(text, name) - return signature - else: - return '' - - #---- IPython private methods --------------------------------------------- - def _context_menu_make(self, pos): - """Reimplement the IPython context menu""" - menu = super(IPythonShellWidget, self)._context_menu_make(pos) - return self.ipyclient.add_actions_to_context_menu(menu) - - def _banner_default(self): - """ - Reimplement banner creation to let the user decide if he wants a - banner or not - """ - banner_o = CONF.get('ipython_console', 'show_banner', True) - if banner_o: - return self.long_banner() - else: - return self.short_banner() - - def _handle_object_info_reply(self, rep): - """ - Reimplement call tips to only show signatures, using the same style - from our Editor and External Console too - Note: For IPython 2- - """ - self.log.debug("oinfo: %s", rep.get('content', '')) - cursor = self._get_cursor() - info = self._request_info.get('call_tip') - if info and info.id == rep['parent_header']['msg_id'] and \ - info.pos == cursor.position(): - content = rep['content'] - if content.get('ismagic', False): - call_info, doc = None, None - else: - call_info, doc = call_tip(content, format_call=True) - if call_info is None and doc is not None: - name = content['name'].split('.')[-1] - argspec = getargspecfromtext(doc) - if argspec: - # This covers cases like np.abs, whose docstring is - # the same as np.absolute and because of that a proper - # signature can't be obtained correctly - call_info = name + argspec - else: - call_info = getsignaturefromtext(doc, name) - if call_info: - self._control.show_calltip(_("Arguments"), call_info, - signature=True, color='#2D62FF') - - def _handle_inspect_reply(self, rep): - """ - Reimplement call tips to only show signatures, using the same style - from our Editor and External Console too - Note: For IPython 3+ - """ - cursor = self._get_cursor() - info = self._request_info.get('call_tip') - if info and info.id == rep['parent_header']['msg_id'] and \ - info.pos == cursor.position(): - content = rep['content'] - if content.get('status') == 'ok' and content.get('found', False): - signature = self.get_signature(content) - if signature: - self._control.show_calltip(_("Arguments"), signature, - signature=True, color='#2D62FF') - - #---- Qt methods ---------------------------------------------------------- - def focusInEvent(self, event): - """Reimplement Qt method to send focus change notification""" - self.emit(SIGNAL('focus_changed()')) - return super(IPythonShellWidget, self).focusInEvent(event) - - def focusOutEvent(self, event): - """Reimplement Qt method to send focus change notification""" - self.emit(SIGNAL('focus_changed()')) - return super(IPythonShellWidget, self).focusOutEvent(event) - - -#----------------------------------------------------------------------------- -# Client widget -#----------------------------------------------------------------------------- -class IPythonClient(QWidget, SaveHistoryMixin): - """ - IPython client or frontend for Spyder - - This is a widget composed of a shell widget (i.e. RichIPythonWidget - + our additions = IPythonShellWidget) and an WebView info widget to - print kernel error and other messages. - """ - - SEPARATOR = '%s##---(%s)---' % (os.linesep*2, time.ctime()) - - def __init__(self, plugin, name, history_filename, connection_file=None, - hostname=None, sshkey=None, password=None, - kernel_widget_id=None, menu_actions=None): - super(IPythonClient, self).__init__(plugin) - SaveHistoryMixin.__init__(self) - self.options_button = None - - # stop button and icon - self.stop_button = None - self.stop_icon = get_icon("stop.png") - - self.connection_file = connection_file - self.kernel_widget_id = kernel_widget_id - self.hostname = hostname - self.sshkey = sshkey - self.password = password - self.name = name - self.get_option = plugin.get_option - self.shellwidget = IPythonShellWidget(config=self.shellwidget_config(), - local_kernel=False) - self.shellwidget.hide() - self.infowidget = WebView(self) - self.menu_actions = menu_actions - self.history_filename = get_conf_path(history_filename) - self.history = [] - self.namespacebrowser = None - - self.set_infowidget_font() - self.loading_page = self._create_loading_page() - self.infowidget.setHtml(self.loading_page) - - vlayout = QVBoxLayout() - toolbar_buttons = self.get_toolbar_buttons() - hlayout = QHBoxLayout() - for button in toolbar_buttons: - hlayout.addWidget(button) - vlayout.addLayout(hlayout) - vlayout.setContentsMargins(0, 0, 0, 0) - vlayout.addWidget(self.shellwidget) - vlayout.addWidget(self.infowidget) - self.setLayout(vlayout) - - self.exit_callback = lambda: plugin.close_client(client=self) - - #------ Public API -------------------------------------------------------- - def show_shellwidget(self, give_focus=True): - """Show shellwidget and configure it""" - self.infowidget.hide() - self.shellwidget.show() - self.infowidget.setHtml(BLANK) - if give_focus: - self.get_control().setFocus() - - # Connect shellwidget to the client - self.shellwidget.set_ipyclient(self) - - # To save history - self.shellwidget.executing.connect(self.add_to_history) - - # For Mayavi to run correctly - self.shellwidget.executing.connect(self.set_backend_for_mayavi) - - # To update history after execution - self.shellwidget.executed.connect(self.update_history) - - # To update the Variable Explorer after execution - self.shellwidget.executed.connect(self.auto_refresh_namespacebrowser) - - # To show a stop button, when executing a process - self.shellwidget.executing.connect(self.enable_stop_button) - - # To hide a stop button after execution stopped - self.shellwidget.executed.connect(self.disable_stop_button) - - def enable_stop_button(self): - self.stop_button.setEnabled(True) - - def disable_stop_button(self): - self.stop_button.setDisabled(True) - - def stop_button_click_handler(self): - self.stop_button.setDisabled(True) - # Interrupt computations or stop debugging - if not self.shellwidget._reading: - self.interrupt_kernel() - else: - self.shellwidget.write_to_stdin('exit') - - def show_kernel_error(self, error): - """Show kernel initialization errors in infowidget""" - # Remove explanation about how to kill the kernel (doesn't apply to us) - error = error.split('issues/2049')[-1] - # Remove unneeded blank lines at the beginning - eol = sourcecode.get_eol_chars(error) - if eol: - error = error.replace(eol, '
    ') - while error.startswith('
    '): - error = error[4:] - # Remove connection message - if error.startswith('To connect another client') or \ - error.startswith('[IPKernelApp] To connect another client'): - error = error.split('
    ') - error = '
    '.join(error[2:]) - # Don't break lines in hyphens - # From http://stackoverflow.com/q/7691569/438386 - error = error.replace('-', '‑') - - message = _("An error ocurred while starting the kernel") - kernel_error_template = Template(KERNEL_ERROR) - page = kernel_error_template.substitute(css_path=CSS_PATH, - message=message, - error=error) - self.infowidget.setHtml(page) - - def show_restart_animation(self): - self.shellwidget.hide() - self.infowidget.setHtml(self.loading_page) - self.infowidget.show() - - def get_name(self): - """Return client name""" - return ((_("Console") if self.hostname is None else self.hostname) - + " " + self.name) - - def get_control(self): - """Return the text widget (or similar) to give focus to""" - # page_control is the widget used for paging - page_control = self.shellwidget._page_control - if page_control and page_control.isVisible(): - return page_control - else: - return self.shellwidget._control - - def get_options_menu(self): - """Return options menu""" - restart_action = create_action(self, _("Restart kernel"), - shortcut=QKeySequence("Ctrl+."), - icon=get_icon('restart.png'), - triggered=self.restart_kernel) - restart_action.setShortcutContext(Qt.WidgetWithChildrenShortcut) - - # Main menu - if self.menu_actions is not None: - actions = [restart_action, None] + self.menu_actions - else: - actions = [restart_action] - return actions - - def get_toolbar_buttons(self): - """Return toolbar buttons list""" - #TODO: Eventually add some buttons (Empty for now) - # (see for example: spyderlib/widgets/externalshell/baseshell.py) - buttons = [] - # Code to add the stop button - if self.stop_button is None: - self.stop_button = create_toolbutton(self, text=_("Stop"), - icon=self.stop_icon, - tip=_("Stop the current command")) - self.disable_stop_button() - # set click event handler - self.stop_button.clicked.connect(self.stop_button_click_handler) - if self.stop_button is not None: - buttons.append(self.stop_button) - - if self.options_button is None: - options = self.get_options_menu() - if options: - self.options_button = create_toolbutton(self, - text=_("Options"), icon=get_icon('tooloptions.png')) - self.options_button.setPopupMode(QToolButton.InstantPopup) - menu = QMenu(self) - add_actions(menu, options) - self.options_button.setMenu(menu) - if self.options_button is not None: - buttons.append(self.options_button) - - return buttons - - def add_actions_to_context_menu(self, menu): - """Add actions to IPython widget context menu""" - # See spyderlib/widgets/ipython.py for more details on this method - inspect_action = create_action(self, _("Inspect current object"), - QKeySequence(get_shortcut('console', - 'inspect current object')), - icon=get_std_icon('MessageBoxInformation'), - triggered=self.inspect_object) - clear_line_action = create_action(self, _("Clear line or block"), - QKeySequence("Shift+Escape"), - icon=get_icon('eraser.png'), - triggered=self.clear_line) - clear_console_action = create_action(self, _("Clear console"), - QKeySequence(get_shortcut('console', - 'clear shell')), - icon=get_icon('clear.png'), - triggered=self.clear_console) - quit_action = create_action(self, _("&Quit"), icon='exit.png', - triggered=self.exit_callback) - add_actions(menu, (None, inspect_action, clear_line_action, - clear_console_action, None, quit_action)) - return menu - - def set_font(self, font): - """Set IPython widget's font""" - self.shellwidget._control.setFont(font) - self.shellwidget.font = font - - def set_infowidget_font(self): - font = get_font('inspector', 'rich_text') - self.infowidget.set_font(font) - - def interrupt_kernel(self): - """Interrupt the associanted Spyder kernel if it's running""" - self.shellwidget.request_interrupt_kernel() - - def restart_kernel(self): - """Restart the associanted Spyder kernel""" - self.shellwidget.request_restart_kernel() - - def inspect_object(self): - """Show how to inspect an object with our object inspector""" - self.shellwidget._control.inspect_current_object() - - def clear_line(self): - """Clear a console line""" - self.shellwidget._keyboard_quit() - - def clear_console(self): - """Clear the whole console""" - self.shellwidget.execute("%clear") - - def if_kernel_dies(self, t): - """ - Show a message in the console if the kernel dies. - t is the time in seconds between the death and showing the message. - """ - message = _("It seems the kernel died unexpectedly. Use " - "'Restart kernel' to continue using this console.") - self.shellwidget._append_plain_text(message + '\n') - - def update_history(self): - self.history = self.shellwidget._history - - def set_backend_for_mayavi(self, command): - calling_mayavi = False - lines = command.splitlines() - for l in lines: - if not l.startswith('#'): - if 'import mayavi' in l or 'from mayavi' in l: - calling_mayavi = True - break - if calling_mayavi: - message = _("Changing backend to Qt for Mayavi") - self.shellwidget._append_plain_text(message + '\n') - self.shellwidget.execute("%gui inline\n%gui qt") - - def interrupt_message(self): - """ - Print an interrupt message when the client is connected to an external - kernel - """ - message = _("Kernel process is either remote or unspecified. " - "Cannot interrupt") - QMessageBox.information(self, "IPython", message) - - def restart_message(self): - """ - Print a restart message when the client is connected to an external - kernel - """ - message = _("Kernel process is either remote or unspecified. " - "Cannot restart.") - QMessageBox.information(self, "IPython", message) - - def set_namespacebrowser(self, namespacebrowser): - """Set namespace browser widget""" - self.namespacebrowser = namespacebrowser - - def auto_refresh_namespacebrowser(self): - """Refresh namespace browser""" - if self.namespacebrowser: - self.namespacebrowser.refresh_table() - - def shellwidget_config(self): - """Generate a Config instance for shell widgets using our config - system - - This lets us create each widget with its own config (as opposed to - IPythonQtConsoleApp, where all widgets have the same config) - """ - # ---- IPython config ---- - try: - profile_path = osp.join(get_ipython_dir(), 'profile_default') - full_ip_cfg = load_pyconfig_files(['ipython_qtconsole_config.py'], - profile_path) - - # From the full config we only select the IPythonWidget section - # because the others have no effect here. - ip_cfg = Config({'IPythonWidget': full_ip_cfg.IPythonWidget}) - except: - ip_cfg = Config() - - # ---- Spyder config ---- - spy_cfg = Config() - - # Make the pager widget a rich one (i.e a QTextEdit) - spy_cfg.IPythonWidget.kind = 'rich' - - # Gui completion widget - gui_comp_o = self.get_option('use_gui_completion') - completions = {True: 'droplist', False: 'ncurses'} - spy_cfg.IPythonWidget.gui_completion = completions[gui_comp_o] - - # Pager - pager_o = self.get_option('use_pager') - if pager_o: - spy_cfg.IPythonWidget.paging = 'inside' - else: - spy_cfg.IPythonWidget.paging = 'none' - - # Calltips - calltips_o = self.get_option('show_calltips') - spy_cfg.IPythonWidget.enable_calltips = calltips_o - - # Buffer size - buffer_size_o = self.get_option('buffer_size') - spy_cfg.IPythonWidget.buffer_size = buffer_size_o - - # Prompts - in_prompt_o = self.get_option('in_prompt') - out_prompt_o = self.get_option('out_prompt') - if in_prompt_o: - spy_cfg.IPythonWidget.in_prompt = in_prompt_o - if out_prompt_o: - spy_cfg.IPythonWidget.out_prompt = out_prompt_o - - # Merge IPython and Spyder configs. Spyder prefs will have prevalence - # over IPython ones - ip_cfg._merge(spy_cfg) - return ip_cfg - - #------ Private API ------------------------------------------------------- - def _create_loading_page(self): - loading_template = Template(LOADING) - loading_img = get_image_path('loading_sprites.png') - if os.name == 'nt': - loading_img = loading_img.replace('\\', '/') - message = _("Connecting to kernel...") - page = loading_template.substitute(css_path=CSS_PATH, - loading_img=loading_img, - message=message) - return page - - #---- Qt methods ---------------------------------------------------------- - def closeEvent(self, event): - """ - Reimplement Qt method to stop sending the custom_restart_kernel_died - signal - """ - kc = self.shellwidget.kernel_client - if kc is not None: - kc.hb_channel.pause() diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/widgets/mixins.py spyder-3.0.2+dfsg1/spyder/widgets/mixins.py --- spyder-2.3.8+dfsg1/spyder/widgets/mixins.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/widgets/mixins.py 2016-10-25 02:05:23.000000000 +0200 @@ -1,44 +1,47 @@ # -*- coding: utf-8 -*- # -# Copyright © 2012 Pierre Raybaut +# Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) +# (see spyder/__init__.py for details) """Mix-in classes These classes were created to be able to provide Spyder's regular text and -console widget features to an independant widget based on QTextEdit for the +console widget features to an independant widget based on QTextEdit for the IPython console plugin. """ +# Standard library imports +from xml.sax.saxutils import escape import os import re import sre_constants import textwrap -from xml.sax.saxutils import escape -from spyderlib.qt.QtGui import (QTextCursor, QTextDocument, QApplication, - QCursor, QToolTip) -from spyderlib.qt.QtCore import Qt, QPoint, QRegExp, SIGNAL +# Third party imports +from qtpy.QtCore import QPoint, QRegExp, Qt +from qtpy.QtGui import QCursor, QTextCursor, QTextDocument +from qtpy.QtWidgets import QApplication, QToolTip # Local imports -from spyderlib.baseconfig import _ -from spyderlib.utils import encoding, sourcecode -from spyderlib.utils.misc import get_error_match -from spyderlib.utils.dochelpers import (getobj, getargspecfromtext, - getsignaturefromtext) -from spyderlib.py3compat import is_text_string, to_text_string, u +from spyder.config.base import _ +from spyder.py3compat import is_text_string, to_text_string, u +from spyder.utils import encoding, sourcecode +from spyder.utils.dochelpers import (getargspecfromtext, getobj, + getsignaturefromtext) +from spyder.utils.misc import get_error_match +from spyder.widgets.arraybuilder import NumpyArrayDialog HISTORY_FILENAMES = [] class BaseEditMixin(object): + def __init__(self): self.eol_chars = None self.calltip_size = 600 - #------Line number area def get_linenumberarea_width(self): """Return line number area width""" @@ -495,9 +498,56 @@ return True return False + def is_editor(self): + """Needs to be overloaded in the codeeditor where it will be True""" + return False + + # --- Numpy matrix/array helper / See 'spyder/widgets/arraybuilder.py' + def enter_array_inline(self): + """ """ + self._enter_array(True) + + def enter_array_table(self): + """ """ + self._enter_array(False) + + def _enter_array(self, inline): + """ """ + offset = self.get_position('cursor') - self.get_position('sol') + rect = self.cursorRect() + dlg = NumpyArrayDialog(self, inline, offset) + + # TODO: adapt to font size + x = rect.left() + x = x + self.get_linenumberarea_width() - 14 + y = rect.top() + (rect.bottom() - rect.top())/2 + y = y - dlg.height()/2 - 3 + + pos = QPoint(x, y) + dlg.move(self.mapToGlobal(pos)) + + # called from editor + if self.is_editor(): + python_like_check = self.is_python_like() + suffix = '\n' + # called from a console + else: + python_like_check = True + suffix = '' + + if python_like_check and dlg.exec_(): + text = dlg.text() + suffix + if text != '': + cursor = self.textCursor() + cursor.beginEditBlock() + cursor.insertText(text) + cursor.endEditBlock() + class TracebackLinksMixin(object): + """ """ QT_CLASS = None + go_to_error = None def __init__(self): self.__cursor_changed = False @@ -509,7 +559,8 @@ self.QT_CLASS.mouseReleaseEvent(self, event) text = self.get_line_at(event.pos()) if get_error_match(text) and not self.has_selected_text(): - self.emit(SIGNAL("go_to_error(QString)"), text) + if self.go_to_error is not None: + self.go_to_error.emit(text) def mouseMoveEvent(self, event): """Show Pointing Hand Cursor on error messages""" @@ -533,19 +584,18 @@ self.QT_CLASS.leaveEvent(self, event) -class InspectObjectMixin(object): +class GetHelpMixin(object): def __init__(self): - self.inspector = None - self.inspector_enabled = False - - def set_inspector(self, inspector): - """Set ObjectInspector DockWidget reference""" - self.inspector = inspector - self.inspector.set_shell(self) + self.help = None + self.help_enabled = False + + def set_help(self, help_plugin): + """Set Help DockWidget reference""" + self.help = help_plugin + + def set_help_enabled(self, state): + self.help_enabled = state - def set_inspector_enabled(self, state): - self.inspector_enabled = state - def inspect_current_object(self): text = '' text1 = self.get_text('sol', 'cursor') @@ -558,22 +608,25 @@ text += tl2[0] if text: self.show_object_info(text, force=True) - + def show_object_info(self, text, call=False, force=False): - """Show signature calltip and/or docstring in the Object Inspector""" - text = to_text_string(text) # Useful only for ExternalShellBase - + """Show signature calltip and/or docstring in the Help plugin""" + text = to_text_string(text) + # Show docstring - insp_enabled = self.inspector_enabled or force - if force and self.inspector is not None: - self.inspector.dockwidget.setVisible(True) - self.inspector.dockwidget.raise_() - if insp_enabled and (self.inspector is not None) and \ - (self.inspector.dockwidget.isVisible()): - # ObjectInspector widget exists and is visible - self.inspector.set_shell(self) - self.inspector.set_object_text(text, ignore_unknown=False) - self.setFocus() # if inspector was not at top level, raising it to + help_enabled = self.help_enabled or force + if force and self.help is not None: + self.help.dockwidget.setVisible(True) + self.help.dockwidget.raise_() + if help_enabled and (self.help is not None) and \ + (self.help.dockwidget.isVisible()): + # Help widget exists and is visible + if hasattr(self, 'get_doc'): + self.help.set_shell(self) + else: + self.help.set_shell(self.parent()) + self.help.set_object_text(text, ignore_unknown=False) + self.setFocus() # if help was not at top level, raising it to # top will automatically give it focus because of # the visibility_changed signal, so we must give # focus back to shell @@ -619,6 +672,7 @@ INITHISTORY = None SEPARATOR = None + append_to_history = None def __init__(self): pass @@ -643,5 +697,5 @@ text = self.SEPARATOR + text encoding.write(text, self.history_filename, mode='ab') - self.emit(SIGNAL('append_to_history(QString,QString)'), - self.history_filename, text) + if self.append_to_history is not None: + self.append_to_history.emit(self.history_filename, text) diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/widgets/objecteditor.py spyder-3.0.2+dfsg1/spyder/widgets/objecteditor.py --- spyder-2.3.8+dfsg1/spyder/widgets/objecteditor.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/widgets/objecteditor.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,170 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright © 2009-2010 Pierre Raybaut -# Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) - -""" -Object Editor Dialog based on Qt -""" - -from __future__ import print_function - -from spyderlib.qt.QtCore import QObject, SIGNAL - -# Local imports -from spyderlib.py3compat import is_text_string - - -class DialogKeeper(QObject): - def __init__(self): - QObject.__init__(self) - self.dialogs = {} - self.namespace = None - - def set_namespace(self, namespace): - self.namespace = namespace - - def create_dialog(self, dialog, refname, func): - self.dialogs[id(dialog)] = dialog, refname, func - self.connect(dialog, SIGNAL('accepted()'), - lambda eid=id(dialog): self.editor_accepted(eid)) - self.connect(dialog, SIGNAL('rejected()'), - lambda eid=id(dialog): self.editor_rejected(eid)) - dialog.show() - dialog.activateWindow() - dialog.raise_() - - def editor_accepted(self, dialog_id): - dialog, refname, func = self.dialogs[dialog_id] - self.namespace[refname] = func(dialog) - self.dialogs.pop(dialog_id) - - def editor_rejected(self, dialog_id): - self.dialogs.pop(dialog_id) - -keeper = DialogKeeper() - - -def create_dialog(obj, obj_name): - """Creates the editor dialog and returns a tuple (dialog, func) where func - is the function to be called with the dialog instance as argument, after - quitting the dialog box - - The role of this intermediate function is to allow easy monkey-patching. - (uschmitt suggested this indirection here so that he can monkey patch - oedit to show eMZed related data) - """ - # Local import - from spyderlib.widgets.texteditor import TextEditor - from spyderlib.widgets.dicteditorutils import (ndarray, FakeObject, - Image, is_known_type, - DataFrame, Series) - from spyderlib.widgets.dicteditor import DictEditor - from spyderlib.widgets.arrayeditor import ArrayEditor - if DataFrame is not FakeObject: - from spyderlib.widgets.dataframeeditor import DataFrameEditor - - conv_func = lambda data: data - readonly = not is_known_type(obj) - if isinstance(obj, ndarray) and ndarray is not FakeObject: - dialog = ArrayEditor() - if not dialog.setup_and_check(obj, title=obj_name, - readonly=readonly): - return - elif isinstance(obj, Image) and Image is not FakeObject \ - and ndarray is not FakeObject: - dialog = ArrayEditor() - import numpy as np - data = np.array(obj) - if not dialog.setup_and_check(data, title=obj_name, - readonly=readonly): - return - from spyderlib.pil_patch import Image - conv_func = lambda data: Image.fromarray(data, mode=obj.mode) - elif isinstance(obj, (DataFrame, Series)) and DataFrame is not FakeObject: - dialog = DataFrameEditor() - if not dialog.setup_and_check(obj): - return - elif is_text_string(obj): - dialog = TextEditor(obj, title=obj_name, readonly=readonly) - else: - dialog = DictEditor() - dialog.setup(obj, title=obj_name, readonly=readonly) - - def end_func(dialog): - return conv_func(dialog.get_value()) - - return dialog, end_func - - -def oedit(obj, modal=True, namespace=None): - """Edit the object 'obj' in a GUI-based editor and return the edited copy - (if Cancel is pressed, return None) - - The object 'obj' is a container - - Supported container types: - dict, list, tuple, str/unicode or numpy.array - - (instantiate a new QApplication if necessary, - so it can be called directly from the interpreter) - """ - # Local import - from spyderlib.utils.qthelpers import qapplication - app = qapplication() - - if modal: - obj_name = '' - else: - assert is_text_string(obj) - obj_name = obj - if namespace is None: - namespace = globals() - keeper.set_namespace(namespace) - obj = namespace[obj_name] - # keep QApplication reference alive in the Python interpreter: - namespace['__qapp__'] = app - - result = create_dialog(obj, obj_name) - if result is None: - return - dialog, end_func = result - - if modal: - if dialog.exec_(): - return end_func(dialog) - else: - keeper.create_dialog(dialog, obj_name, end_func) - import os - if os.name == 'nt': - app.exec_() - - -def test(): - """Run object editor test""" - import datetime, numpy as np - from spyderlib.pil_patch import Image - image = Image.fromarray(np.random.random_integers(255, size=(100, 100))) - example = {'str': 'kjkj kj k j j kj k jkj', - 'list': [1, 3, 4, 'kjkj', None], - 'dict': {'d': 1, 'a': np.random.rand(10, 10), 'b': [1, 2]}, - 'float': 1.2233, - 'array': np.random.rand(10, 10), - 'image': image, - 'date': datetime.date(1945, 5, 8), - 'datetime': datetime.datetime(1945, 5, 8), - } - image = oedit(image) - class Foobar(object): - def __init__(self): - self.text = "toto" - foobar = Foobar() - print(oedit(foobar)) - print(oedit(example)) - print(oedit(np.random.rand(10, 10))) - print(oedit(oedit.__doc__)) - print(example) - -if __name__ == "__main__": - test() diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/widgets/onecolumntree.py spyder-3.0.2+dfsg1/spyder/widgets/onecolumntree.py --- spyder-2.3.8+dfsg1/spyder/widgets/onecolumntree.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/widgets/onecolumntree.py 2016-10-25 02:05:23.000000000 +0200 @@ -1,26 +1,18 @@ # -*- coding: utf-8 -*- # -# Copyright © 2009-2010 Pierre Raybaut +# Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) +# (see spyder/__init__.py for details) -""" -spyderlib.widgets -================= - -Widgets defined in this module may be used in any other Qt-based application - -They are also used in Spyder through the Plugin interface -(see spyderlib.plugins) -""" - -from spyderlib.qt.QtGui import QTreeWidget, QMenu -from spyderlib.qt.QtCore import SIGNAL +# Third party imports +from qtpy.QtCore import Slot +from qtpy.QtWidgets import QTreeWidget, QMenu # Local imports -from spyderlib.baseconfig import _ -from spyderlib.utils.qthelpers import (get_icon, create_action, add_actions, - get_item_user_text) +from spyder.config.base import _ +from spyder.utils import icon_manager as ima +from spyder.utils.qthelpers import (add_actions, create_action, + get_item_user_text) class OneColumnTree(QTreeWidget): @@ -29,10 +21,8 @@ QTreeWidget.__init__(self, parent) self.setItemsExpandable(True) self.setColumnCount(1) - self.connect(self, SIGNAL('itemActivated(QTreeWidgetItem*,int)'), - self.activated) - self.connect(self, SIGNAL('itemClicked(QTreeWidgetItem*,int)'), - self.clicked) + self.itemActivated.connect(self.activated) + self.itemClicked.connect(self.clicked) # Setup context menu self.menu = QMenu(self) self.collapse_all_action = None @@ -43,8 +33,7 @@ self.__expanded_state = None - self.connect(self, SIGNAL('itemSelectionChanged()'), - self.item_selection_changed) + self.itemSelectionChanged.connect(self.item_selection_changed) self.item_selection_changed() def activated(self, item): @@ -61,24 +50,24 @@ """Setup context menu common actions""" self.collapse_all_action = create_action(self, text=_('Collapse all'), - icon=get_icon('collapse.png'), + icon=ima.icon('collapse'), triggered=self.collapseAll) self.expand_all_action = create_action(self, text=_('Expand all'), - icon=get_icon('expand.png'), + icon=ima.icon('expand'), triggered=self.expandAll) self.restore_action = create_action(self, text=_('Restore'), tip=_('Restore original tree layout'), - icon=get_icon('restore.png'), + icon=ima.icon('restore'), triggered=self.restore) self.collapse_selection_action = create_action(self, text=_('Collapse selection'), - icon=get_icon('collapse_selection.png'), + icon=ima.icon('collapse_selection'), triggered=self.collapse_selection) self.expand_selection_action = create_action(self, text=_('Expand selection'), - icon=get_icon('expand_selection.png'), + icon=ima.icon('expand_selection'), triggered=self.expand_selection) return [self.collapse_all_action, self.expand_all_action, self.restore_action, None, @@ -98,6 +87,7 @@ # (reimplement this method) return [] + @Slot() def restore(self): self.collapseAll() for item in self.get_top_level_items(): @@ -114,7 +104,8 @@ for index in range(item.childCount()): child = item.child(index) self.__expand_item(child) - + + @Slot() def expand_selection(self): items = self.selectedItems() if not items: @@ -130,6 +121,7 @@ child = item.child(index) self.__collapse_item(child) + @Slot() def collapse_selection(self): items = self.selectedItems() if not items: diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/widgets/pathmanager.py spyder-3.0.2+dfsg1/spyder/widgets/pathmanager.py --- spyder-2.3.8+dfsg1/spyder/widgets/pathmanager.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/widgets/pathmanager.py 2016-10-25 02:05:23.000000000 +0200 @@ -1,30 +1,34 @@ # -*- coding: utf-8 -*- # -# Copyright © 2009-2010 Pierre Raybaut +# Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) +# (see spyder/__init__.py for details) """Spyder path manager""" +# Standard library imports from __future__ import print_function - -from spyderlib.qt.QtGui import (QDialog, QListWidget, QDialogButtonBox, - QVBoxLayout, QHBoxLayout, QMessageBox, - QListWidgetItem) -from spyderlib.qt.QtCore import Qt, SIGNAL, SLOT -from spyderlib.qt.compat import getexistingdirectory - import os -import sys import os.path as osp +import sys + +# Third party imports +from qtpy.compat import getexistingdirectory +from qtpy.QtCore import Qt, Signal, Slot +from qtpy.QtWidgets import (QDialog, QDialogButtonBox, QHBoxLayout, + QListWidget, QListWidgetItem, QMessageBox, + QVBoxLayout) # Local imports -from spyderlib.utils.qthelpers import get_icon, get_std_icon, create_toolbutton -from spyderlib.baseconfig import _ -from spyderlib.py3compat import getcwd +from spyder.config.base import _ +from spyder.py3compat import getcwd +from spyder.utils import icon_manager as ima +from spyder.utils.qthelpers import create_toolbutton class PathManager(QDialog): + redirect_stdio = Signal(bool) + def __init__(self, parent=None, pathlist=None, ro_pathlist=None, sync=True): QDialog.__init__(self, parent) @@ -43,7 +47,7 @@ self.last_path = getcwd() self.setWindowTitle(_("PYTHONPATH manager")) - self.setWindowIcon(get_icon('pythonpath.png')) + self.setWindowIcon(ima.icon('pythonpath')) self.resize(500, 300) self.selection_widgets = [] @@ -56,8 +60,7 @@ self.toolbar_widgets1 = self.setup_top_toolbar(top_layout) self.listwidget = QListWidget(self) - self.connect(self.listwidget, SIGNAL("currentRowChanged(int)"), - self.refresh) + self.listwidget.currentRowChanged.connect(self.refresh) layout.addWidget(self.listwidget) bottom_layout = QHBoxLayout() @@ -67,7 +70,7 @@ # Buttons configuration bbox = QDialogButtonBox(QDialogButtonBox.Close) - self.connect(bbox, SIGNAL("rejected()"), SLOT("reject()")) + bbox.rejected.connect(self.reject) bottom_layout.addWidget(bbox) self.update_list() @@ -82,25 +85,25 @@ toolbar = [] movetop_button = create_toolbutton(self, text=_("Move to top"), - icon=get_icon('2uparrow.png'), + icon=ima.icon('2uparrow'), triggered=lambda: self.move_to(absolute=0), text_beside_icon=True) toolbar.append(movetop_button) moveup_button = create_toolbutton(self, text=_("Move up"), - icon=get_icon('1uparrow.png'), + icon=ima.icon('1uparrow'), triggered=lambda: self.move_to(relative=-1), text_beside_icon=True) toolbar.append(moveup_button) movedown_button = create_toolbutton(self, text=_("Move down"), - icon=get_icon('1downarrow.png'), + icon=ima.icon('1downarrow'), triggered=lambda: self.move_to(relative=1), text_beside_icon=True) toolbar.append(movedown_button) movebottom_button = create_toolbutton(self, text=_("Move to bottom"), - icon=get_icon('2downarrow.png'), + icon=ima.icon('2downarrow'), triggered=lambda: self.move_to(absolute=1), text_beside_icon=True) toolbar.append(movebottom_button) @@ -110,13 +113,13 @@ def setup_bottom_toolbar(self, layout, sync=True): toolbar = [] - add_button = create_toolbutton(self, text=_("Add path"), - icon=get_icon('edit_add.png'), + add_button = create_toolbutton(self, text=_('Add path'), + icon=ima.icon('edit_add'), triggered=self.add_path, text_beside_icon=True) toolbar.append(add_button) - remove_button = create_toolbutton(self, text=_("Remove path"), - icon=get_icon('edit_remove.png'), + remove_button = create_toolbutton(self, text=_('Remove path'), + icon=ima.icon('edit_remove'), triggered=self.remove_path, text_beside_icon=True) toolbar.append(remove_button) @@ -126,13 +129,14 @@ if os.name == 'nt' and sync: self.sync_button = create_toolbutton(self, text=_("Synchronize..."), - icon=get_icon('synchronize.png'), triggered=self.synchronize, + icon=ima.icon('fileimport'), triggered=self.synchronize, tip=_("Synchronize Spyder's path list with PYTHONPATH " "environment variable"), text_beside_icon=True) layout.addWidget(self.sync_button) return toolbar - + + @Slot() def synchronize(self): """ Synchronize Spyder's path list with PYTHONPATH environment variable @@ -152,8 +156,8 @@ remove = True else: remove = False - from spyderlib.utils.environ import (get_user_env, set_user_env, - listdict2envdict) + from spyder.utils.environ import (get_user_env, set_user_env, + listdict2envdict) env = get_user_env() if remove: ppath = self.pathlist+self.ro_pathlist @@ -176,7 +180,7 @@ self.listwidget.clear() for name in self.pathlist+self.ro_pathlist: item = QListWidgetItem(name) - item.setIcon(get_std_icon('DirClosedIcon')) + item.setIcon(ima.icon('DirClosedIcon')) if name in self.ro_pathlist: item.setFlags(Qt.NoItemFlags) self.listwidget.addItem(item) @@ -204,7 +208,8 @@ self.pathlist.insert(new_index, path) self.update_list() self.listwidget.setCurrentRow(new_index) - + + @Slot() def remove_path(self): answer = QMessageBox.warning(self, _("Remove path"), _("Do you really want to remove selected path?"), @@ -212,12 +217,13 @@ if answer == QMessageBox.Yes: self.pathlist.pop(self.listwidget.currentRow()) self.update_list() - + + @Slot() def add_path(self): - self.emit(SIGNAL('redirect_stdio(bool)'), False) + self.redirect_stdio.emit(False) directory = getexistingdirectory(self, _("Select directory"), self.last_path) - self.emit(SIGNAL('redirect_stdio(bool)'), True) + self.redirect_stdio.emit(True) if directory: directory = osp.abspath(directory) self.last_path = directory @@ -237,11 +243,13 @@ def test(): """Run path manager test""" - from spyderlib.utils.qthelpers import qapplication + from spyder.utils.qthelpers import qapplication _app = qapplication() # analysis:ignore - test = PathManager(None, sys.path[:-10], sys.path[-10:]) + test = PathManager(None, pathlist=sys.path[:-10], + ro_pathlist=sys.path[-10:]) test.exec_() print(test.get_path_list()) + if __name__ == "__main__": test() diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/widgets/projectexplorer.py spyder-3.0.2+dfsg1/spyder/widgets/projectexplorer.py --- spyder-2.3.8+dfsg1/spyder/widgets/projectexplorer.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/widgets/projectexplorer.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,1368 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright © 2010-2011 Pierre Raybaut -# Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) - -"""Project Explorer""" - -# pylint: disable=C0103 - -from __future__ import print_function - -from spyderlib.qt.QtGui import (QVBoxLayout, QLabel, QHBoxLayout, QWidget, - QFileIconProvider, QMessageBox, QInputDialog, - QLineEdit, QPushButton, QHeaderView, - QAbstractItemView) -from spyderlib.qt.QtCore import Qt, SIGNAL, QFileInfo, Slot, Signal -from spyderlib.qt.compat import getexistingdirectory - -import os -import re -import shutil -import os.path as osp -import xml.etree.ElementTree as ElementTree - -# Local imports -from spyderlib.utils import misc -from spyderlib.utils.qthelpers import get_icon, get_std_icon, create_action -from spyderlib.baseconfig import _, STDERR, get_image_path -from spyderlib.widgets.explorer import FilteredDirView, listdir, fixpath -from spyderlib.widgets.formlayout import fedit -from spyderlib.widgets.pathmanager import PathManager -from spyderlib.py3compat import to_text_string, getcwd, pickle - - -def has_children_files(path, include, exclude, show_all): - """Return True if path has children files""" - try: - return len( listdir(path, include, exclude, show_all) ) > 0 - except (IOError, OSError): - return False - - -def is_drive_path(path): - """Return True if path is a drive (Windows)""" - path = osp.abspath(path) - return osp.normpath(osp.join(path, osp.pardir)) == path - - -def get_dir_icon(dirname, project): - """Return appropriate directory icon""" - if is_drive_path(dirname): - return get_std_icon('DriveHDIcon') - prefix = 'pp_' if dirname in project.get_pythonpath() else '' - if dirname == project.root_path: - if project.is_opened(): - return get_icon(prefix+'project.png') - else: - return get_icon('project_closed.png') - elif osp.isfile(osp.join(dirname, '__init__.py')): - return get_icon(prefix+'package.png') - else: - return get_icon(prefix+'folder.png') - - -class Project(object): - """Spyder project""" - CONFIG_NAME = '.spyderproject' - CONFIG_ATTR = ('name', 'related_projects', 'relative_pythonpath', 'opened') - - def __init__(self): - self.name = None - self.root_path = None - self.related_projects = [] # storing project path, not project objects - self.pythonpath = [] - self.opened = True - self.ioerror_flag = False - - def set_root_path(self, root_path): - """Set workspace root path""" - if self.name is None: - self.name = osp.basename(root_path) - self.root_path = to_text_string(root_path) - config_path = self.__get_project_config_path() - if osp.exists(config_path): - self.load() - else: - if not osp.isdir(self.root_path): - os.mkdir(self.root_path) - self.save() - - def rename(self, new_name): - """Rename project and rename its root path accordingly""" - old_name = self.name - self.name = new_name - pypath = self.relative_pythonpath - self.root_path = self.root_path[:-len(old_name)]+new_name - self.relative_pythonpath = pypath - self.save() - - def _get_relative_pythonpath(self): - """Return PYTHONPATH list as relative paths""" - # Workaround to replace os.path.relpath (new in Python v2.6): - offset = len(self.root_path)+len(os.pathsep) - return [path[offset:] for path in self.pythonpath] - - def _set_relative_pythonpath(self, value): - """Set PYTHONPATH list relative paths""" - self.pythonpath = [osp.abspath(osp.join(self.root_path, path)) - for path in value] - - relative_pythonpath = property(_get_relative_pythonpath, - _set_relative_pythonpath) - - def __get_project_config_path(self): - """Return project configuration path""" - return osp.join(self.root_path, self.CONFIG_NAME) - - def load(self): - """Load project data""" - fname = self.__get_project_config_path() - try: - # Old format (Spyder 2.0-2.1 for Python 2) - with open(fname, 'U') as fdesc: - data = pickle.loads(fdesc.read()) - except (pickle.PickleError, TypeError, UnicodeDecodeError, - AttributeError): - try: - # New format (Spyder >=2.2 for Python 2 and Python 3) - with open(fname, 'rb') as fdesc: - data = pickle.loads(fdesc.read()) - except (IOError, OSError, pickle.PickleError): - self.ioerror_flag = True - return - # Compatibilty with old project explorer file format: - if 'relative_pythonpath' not in data: - print("Warning: converting old configuration file " \ - "for project '%s'" % data['name'], file=STDERR) - self.pythonpath = data['pythonpath'] - data['relative_pythonpath'] = self.relative_pythonpath - for attr in self.CONFIG_ATTR: - setattr(self, attr, data[attr]) - self.save() - - def save(self): - """Save project data""" - data = {} - for attr in self.CONFIG_ATTR: - data[attr] = getattr(self, attr) - try: - with open(self.__get_project_config_path(), 'wb') as fdesc: - pickle.dump(data, fdesc, 2) - except (IOError, OSError): - self.ioerror_flag = True - - def delete(self): - """Delete project""" - os.remove(self.__get_project_config_path()) - - #------Misc. - def get_related_projects(self): - """Return related projects path list""" - return self.related_projects - - def set_related_projects(self, related_projects): - """Set related projects""" - self.related_projects = related_projects - self.save() - - def open(self): - """Open project""" - self.opened = True - self.save() - - def close(self): - """Close project""" - self.opened = False - self.save() - - def is_opened(self): - """Return True if project is opened""" - return self.opened - - def is_file_in_project(self, fname): - """Return True if file *fname* is in one of the project subfolders""" - fixed_root = fixpath(self.root_path) - return fixpath(fname) == fixed_root \ - or fixpath(osp.dirname(fname)).startswith(fixed_root) - - def is_root_path(self, dirname): - """Return True if dirname is project's root path""" - return fixpath(dirname) == fixpath(self.root_path) - - def is_in_pythonpath(self, dirname): - """Return True if dirname is in project's PYTHONPATH""" - return fixpath(dirname) in [fixpath(_p) for _p in self.pythonpath] - - #------Python Path - def get_pythonpath(self): - """Return a copy of pythonpath attribute""" - return self.pythonpath[:] - - def set_pythonpath(self, pythonpath): - """Set project's PYTHONPATH""" - self.pythonpath = pythonpath - self.save() - - def remove_from_pythonpath(self, path): - """Remove path from project's PYTHONPATH - Return True if path was removed, False if it was not found""" - pathlist = self.get_pythonpath() - if path in pathlist: - pathlist.pop(pathlist.index(path)) - self.set_pythonpath(pathlist) - return True - else: - return False - - def add_to_pythonpath(self, path): - """Add path to project's PYTHONPATH - Return True if path was added, False if it was already there""" - pathlist = self.get_pythonpath() - if path in pathlist: - return False - else: - pathlist.insert(0, path) - self.set_pythonpath(pathlist) - return True - - -class Workspace(object): - """Spyder workspace - Set of projects with common root path parent directory""" - CONFIG_NAME = '.spyderworkspace' - CONFIG_ATTR = ('name', 'project_paths', ) - - def __init__(self): - self.name = None - self.root_path = None - self.projects = [] - self.ioerror_flag = False - - def _get_project_paths(self): - """Return workspace projects root path list""" - # Convert project absolute paths to paths relative to Workspace root - offset = len(self.root_path)+len(os.pathsep) - return [proj.root_path[offset:] for proj in self.projects] - - def _set_project_paths(self, pathlist): - """Set workspace projects root path list""" - # Convert paths relative to Workspace root to project absolute paths - for path in pathlist: - if path.startswith(self.root_path): - # do nothing, this is the old Workspace format - root_path = path - else: - root_path = osp.join(self.root_path, path) - self.add_project(root_path) - - project_paths = property(_get_project_paths, _set_project_paths) - - def is_valid(self): - """Return True if workspace is valid (i.e. root path is defined)""" - return self.root_path is not None and osp.isdir(self.root_path) - - def is_empty(self): - """Return True if workspace is empty (i.e. no project)""" - if not self.is_valid(): - return - return len(self.projects) == 0 - - def set_root_path(self, root_path): - """Set workspace root path""" - if self.name is None: - self.name = osp.basename(root_path) - self.root_path = to_text_string(osp.abspath(root_path)) - config_path = self.__get_workspace_config_path() - if osp.exists(config_path): - self.load() - else: - self.save() - - def set_name(self, name): - """Set workspace name""" - self.name = name - self.save() - - def __get_workspace_config_path(self): - """Return project configuration path""" - return osp.join(self.root_path, self.CONFIG_NAME) - - def load(self): - """Load workspace data""" - fname = self.__get_workspace_config_path() - try: - # Old format (Spyder 2.0-2.1 for Python 2) - with open(fname, 'U') as fdesc: - data = pickle.loads(fdesc.read()) - except (pickle.PickleError, TypeError, UnicodeDecodeError): - try: - # New format (Spyder >=2.2 for Python 2 and Python 3) - with open(fname, 'rb') as fdesc: - data = pickle.loads(fdesc.read()) - except (IOError, OSError, pickle.PickleError): - self.ioerror_flag = True - return - for attr in self.CONFIG_ATTR: - setattr(self, attr, data[attr]) - self.save() - - def save(self): - """Save workspace data""" - data = {} - for attr in self.CONFIG_ATTR: - data[attr] = getattr(self, attr) - try: - with open(self.__get_workspace_config_path(), 'wb') as fdesc: - pickle.dump(data, fdesc, 2) - except (IOError, OSError): - self.ioerror_flag = True - - def delete(self): - """Delete workspace""" - os.remove(self.__get_workspace_config_path()) - - #------Misc. - def get_ioerror_warning_message(self): - """Return a warning message if IOError exception was raised when - loading/saving the workspace or one of its projects""" - txt = "" - projlist = [_p.name for _p in self.projects if _p.ioerror_flag] - if self.ioerror_flag: - txt += _("its own configuration file") - if projlist: - txt += _(" and ") - else: - txt += "." - if projlist: - txt += _("the following projects:
    %s") % ", ".join(projlist) - return txt - - def is_file_in_workspace(self, fname): - """Return True if file *fname* is in one of the projects""" - return any([proj.is_file_in_project(fname) for proj in self.projects]) - - def is_file_in_closed_project(self, fname): - """Return True if file *fname* is in one of the closed projects""" - return any([proj.is_file_in_project(fname) for proj in self.projects - if not proj.is_opened()]) - - def is_in_pythonpath(self, dirname): - """Return True if dirname is in workspace's PYTHONPATH""" - return any([proj.is_in_pythonpath(dirname) for proj in self.projects]) - - def has_project(self, root_path_or_name): - """Return True if workspace has a project - with given root path or name""" - checklist = [project.root_path for project in self.projects - ]+[project.name for project in self.projects] - return root_path_or_name in checklist - - def get_source_project(self, fname): - """Return project which contains source *fname*""" - for project in self.projects: - if project.is_file_in_project(fname): - return project - - def get_project_from_name(self, name): - """Return project's object from name""" - for project in self.projects: - if project.name == name: - return project - - def get_folder_names(self): - """Return all project folder names (root path basename)""" - return [osp.basename(proj.root_path) for proj in self.projects] - - def add_project(self, root_path): - """Create project from root path, add it to workspace - Return the created project instance""" - project = Project() - try: - project.set_root_path(root_path) - except OSError: - # This may happens when loading a Workspace with absolute paths - # which has just been moved to a different location - return - self.projects.append(project) - self.save() - - def open_projects(self, projects, open_related=True): - """Open projects""" - for project in projects: - project.open() - related_projects = project.get_related_projects() - if open_related: - for projname in related_projects: - for relproj in self.projects: - if relproj.name == projname: - self.open_projects(relproj, open_related=False) - self.save() - - def close_projects(self, projects): - """Close projects""" - for project in projects: - project.close() - self.save() - - def remove_projects(self, projects): - """Remove projects""" - for project in projects: - project.delete() - self.projects.pop(self.projects.index(project)) - self.save() - - def close_unrelated_projects(self, projects): - """Close unrelated projects""" - unrelated_projects = [] - for project in projects: - for proj in self.projects: - if proj is project: - continue - if proj.name in project.get_related_projects(): - continue - if project.name in proj.get_related_projects(): - continue - unrelated_projects.append(proj) - self.close_projects(unrelated_projects) - self.save() - return unrelated_projects - - def rename_project(self, project, new_name): - """Rename project, update the related projects if necessary""" - old_name = project.name - for proj in self.projects: - relproj = proj.get_related_projects() - if old_name in relproj: - relproj[relproj.index(old_name)] = new_name - proj.set_related_projects(relproj) - project.rename(new_name) - self.save() - - def get_other_projects(self, project): - """Return all projects, except given project""" - return [_p for _p in self.projects if _p is not project] - - #------Python Path - def get_pythonpath(self): - """Return global PYTHONPATH (for all opened projects""" - pythonpath = [] - for project in self.projects: - if project.is_opened(): - pythonpath += project.get_pythonpath() - return pythonpath - - -def get_pydev_project_infos(project_path): - """Return Pydev project infos: name, related projects and PYTHONPATH""" - root = ElementTree.parse(osp.join(project_path, ".pydevproject")) - path = [] - project_root = osp.dirname(project_path) - for element in root.getiterator(): - if element.tag == 'path': - path.append(osp.abspath(osp.join(project_root, element.text[1:]))) - - root = ElementTree.parse(osp.join(project_path, ".project")) - related_projects = [] - name = None - for element in root.getiterator(): - if element.tag == 'project': - related_projects.append(element.text) - elif element.tag == 'name' and name is None: - name = element.text - - return name, related_projects, path - - -class IconProvider(QFileIconProvider): - """Project tree widget icon provider""" - def __init__(self, treeview): - super(IconProvider, self).__init__() - self.treeview = treeview - - @Slot(int) - @Slot(QFileInfo) - def icon(self, icontype_or_qfileinfo): - """Reimplement Qt method""" - if isinstance(icontype_or_qfileinfo, QFileIconProvider.IconType): - return super(IconProvider, self).icon(icontype_or_qfileinfo) - else: - qfileinfo = icontype_or_qfileinfo - fname = osp.normpath(to_text_string(qfileinfo.absoluteFilePath())) - if osp.isdir(fname): - project = self.treeview.get_source_project(fname) - if project is None: - return super(IconProvider, self).icon(qfileinfo) - else: - return get_dir_icon(fname, project) - else: - ext = osp.splitext(fname)[1][1:] - icon_path = get_image_path(ext+'.png', default=None) - if icon_path is not None: - return get_icon(icon_path) - else: - return super(IconProvider, self).icon(qfileinfo) - - -class ExplorerTreeWidget(FilteredDirView): - """Explorer tree widget - - workspace: this is the explorer tree widget root path - (this attribute name is specific to project explorer)""" - def __init__(self, parent, show_hscrollbar=True): - FilteredDirView.__init__(self, parent) - - self.workspace = Workspace() - - self.connect(self.fsmodel, SIGNAL('modelReset()'), - self.reset_icon_provider) - self.reset_icon_provider() - - self.last_folder = None - - self.setSelectionMode(FilteredDirView.ExtendedSelection) - - self.setHeaderHidden(True) - - self.show_hscrollbar = show_hscrollbar - - # Enable drag & drop events - self.setDragEnabled(True) - self.setDragDropMode(FilteredDirView.DragDrop) - - #------DirView API--------------------------------------------------------- - def setup_view(self): - """Setup view""" - FilteredDirView.setup_view(self) - - def create_file_new_actions(self, fnames): - """Return actions for submenu 'New...'""" - new_project_act = create_action(self, text=_('Project...'), - icon=get_icon('project_expanded.png'), - triggered=self.new_project) - if self.workspace.is_empty(): - new_project_act.setText(_('New project...')) - return [new_project_act] - else: - new_actions = FilteredDirView.create_file_new_actions(self, fnames) - return [new_project_act, None]+new_actions - - def create_file_import_actions(self, fnames): - """Return actions for submenu 'Import...'""" - import_folder_act = create_action(self, - text=_('Existing directory'), - icon=get_std_icon('DirOpenIcon'), - triggered=self.import_existing_directory) - import_spyder_act = create_action(self, - text=_('Existing Spyder project'), - icon=get_icon('spyder.svg'), - triggered=self.import_existing_project) - import_pydev_act = create_action(self, - text=_('Existing Pydev project'), - icon=get_icon('pydev.png'), - triggered=self.import_existing_pydev_project) - return [import_folder_act, import_spyder_act, import_pydev_act] - - def create_file_manage_actions(self, fnames): - """Reimplement DirView method""" - only_folders = all([osp.isdir(_fn) for _fn in fnames]) - projects = [self.get_source_project(fname) for fname in fnames] - pjfnames = list(zip(projects, fnames)) - any_project = any([_pr.is_root_path(_fn) for _pr, _fn in pjfnames]) - any_folder_in_path = any([_proj.is_in_pythonpath(_fn) - for _proj, _fn in pjfnames]) - any_folder_not_in_path = only_folders and \ - any([not _proj.is_in_pythonpath(_fn) - for _proj, _fn in pjfnames]) - open_act = create_action(self, - text=_('Open project'), - icon=get_icon('project_expanded.png'), - triggered=lambda: - self.open_projects(projects)) - close_act = create_action(self, - text=_('Close project'), - icon=get_icon('project_closed.png'), - triggered=lambda: - self.close_projects(projects)) - close_unrelated_act = create_action(self, - text=_('Close unrelated projects'), - triggered=lambda: - self.close_unrelated_projects(projects)) - manage_path_act = create_action(self, - icon=get_icon('pythonpath.png'), - text=_('PYTHONPATH manager'), - triggered=lambda: - self.manage_path(projects)) - relproj_act = create_action(self, - text=_('Edit related projects'), - triggered=lambda: - self.edit_related_projects(projects)) - state = self.workspace is not None\ - and len(self.workspace.projects) > 1 - relproj_act.setEnabled(state) - - add_to_path_act = create_action(self, - text=_('Add to PYTHONPATH'), - icon=get_icon('add_to_path.png'), - triggered=lambda: - self.add_to_path(fnames)) - remove_from_path_act = create_action(self, - text=_('Remove from PYTHONPATH'), - icon=get_icon('remove_from_path.png'), - triggered=lambda: - self.remove_from_path(fnames)) - properties_act = create_action(self, - text=_('Properties'), - icon=get_icon('advanced.png'), - triggered=lambda: - self.show_properties(fnames)) - - actions = [] - if any_project: - if any([not _proj.is_opened() for _proj in projects]): - actions += [open_act] - if any([_proj.is_opened() for _proj in projects]): - actions += [close_act, close_unrelated_act] - actions += [manage_path_act, relproj_act, None] - - if only_folders: - if any_folder_not_in_path: - actions += [add_to_path_act] - if any_folder_in_path: - actions += [remove_from_path_act] - actions += [None, properties_act, None] - actions += FilteredDirView.create_file_manage_actions(self, fnames) - return actions - - def create_context_menu_actions(self): - """Reimplement DirView method""" - if self.workspace.is_valid(): - # Workspace's root path is already defined - return FilteredDirView.create_context_menu_actions(self) - else: - return [] - - def setup_common_actions(self): - """Setup context menu common actions""" - actions = FilteredDirView.setup_common_actions(self) - - # Toggle horizontal scrollbar - hscrollbar_action = create_action(self, _("Show horizontal scrollbar"), - toggled=self.toggle_hscrollbar) - hscrollbar_action.setChecked(self.show_hscrollbar) - self.toggle_hscrollbar(self.show_hscrollbar) - - return actions + [hscrollbar_action] - - #------Public API---------------------------------------------------------- - def toggle_hscrollbar(self, checked): - """Toggle horizontal scrollbar""" - self.parent_widget.sig_option_changed.emit('show_hscrollbar', checked) - self.show_hscrollbar = checked - self.header().setStretchLastSection(not checked) - self.header().setHorizontalScrollMode(QAbstractItemView.ScrollPerPixel) - self.header().setResizeMode(QHeaderView.ResizeToContents) - - def set_folder_names(self, folder_names): - """Set folder names""" - self.setUpdatesEnabled(False) - FilteredDirView.set_folder_names(self, folder_names) - self.reset_icon_provider() - self.setUpdatesEnabled(True) - - def reset_icon_provider(self): - """Reset file system model icon provider - The purpose of this is to refresh files/directories icons""" - self.fsmodel.setIconProvider(IconProvider(self)) - - def check_for_io_errors(self): - """Eventually show a warning message box if IOError exception was - raised when loading/saving the workspace or one of its projects""" - txt = self.workspace.get_ioerror_warning_message() - if txt: - QMessageBox.critical(self, _('Workspace'), - _("The workspace was unable to load or save %s

    " - "Please check if you have the permission to write the " - "associated configuration files.") % txt) - - def set_workspace(self, root_path): - """Set project explorer's workspace directory""" - self.workspace = Workspace() - self.setModel(None) - self.fsmodel = None - self.proxymodel = None - self.setup_fs_model() - self.setup_proxy_model() - self.workspace.set_root_path(root_path) - self.set_root_path(root_path) - for index in range(1, self.model().columnCount()): - self.hideColumn(index) - self.set_folder_names(self.workspace.get_folder_names()) - - # The following fixes Issue 952: if we don't reset the "show all" - # option here, we will lose the feature because we have just rebuilt - # the fsmodel object from scratch. This would happen in particular - # when setting the workspace option in the project explorer widget - # (see spyderlib/widgets/projectexplorer.py). - self.set_show_all(self.show_all) - - self.parent_widget.emit(SIGNAL("pythonpath_changed()")) -# print "folders:", self.workspace.get_folder_names() -# print "is_valid:", self.workspace.is_valid() -# print "is_empty:", self.workspace.is_empty() - - def get_workspace(self): - """Return project explorer's workspace directory""" - return self.workspace.root_path - - def is_in_workspace(self, fname): - """Return True if file/directory is in workspace""" - return self.workspace.is_file_in_workspace(fname) - - def get_project_path_from_name(self, name): - """Return project root path from name, knowing the workspace path""" - return osp.join(self.get_workspace(), name) - - def get_source_project(self, fname): - """Return project which contains source *fname*""" - return self.workspace.get_source_project(fname) - - def get_project_from_name(self, name): - """Return project's object from name""" - return self.workspace.get_project_from_name(name) - - def get_pythonpath(self): - """Return global PYTHONPATH (for all opened projects""" - return self.workspace.get_pythonpath() - - def add_project(self, folder, silent=False): - """Add project to tree""" - if not self.is_valid_project_root_path(folder, silent=silent): - return - if not fixpath(folder).startswith(fixpath(self.root_path)): - title = _("Import directory") - answer = QMessageBox.warning(self, title, - _("The following directory is not in workspace:" - "
    %s

    " - "Do you want to continue (and copy the " - "directory to workspace)?") % folder, - QMessageBox.Yes|QMessageBox.No) - if answer == QMessageBox.No: - return - name = self._select_project_name(title, - default=osp.basename(folder)) - if name is None: - return - dst = self.get_project_path_from_name(name) - try: - shutil.copytree(folder, dst) - except EnvironmentError as error: - QMessageBox.critical(self, title, - _("Unable to %s %s" - "

    Error message:
    %s" - ) % (_('copy'), folder, - to_text_string(error))) - folder = dst - - project = self.workspace.add_project(folder) - self.set_folder_names(self.workspace.get_folder_names()) - self.parent_widget.emit(SIGNAL("pythonpath_changed()")) - - self.check_for_io_errors() - - return project - - def open_projects(self, projects, open_related=True): - """Open projects""" - self.workspace.open_projects(projects, open_related) - self.parent_widget.emit(SIGNAL("pythonpath_changed()")) - self.reset_icon_provider() - for project in projects: - self.update(self.get_index(project.root_path)) - - def close_projects(self, projects): - """Close projects""" - self.workspace.close_projects(projects) - self.parent_widget.emit(SIGNAL("pythonpath_changed()")) - self.parent_widget.emit(SIGNAL("projects_were_closed()")) - self.reset_icon_provider() - for project in projects: - index = self.get_index(project.root_path) - self.update(index) - self.collapse(index) - - def remove_projects(self, projects): - """Remove projects""" - self.workspace.remove_projects(projects) - self.set_folder_names(self.workspace.get_folder_names()) - self.parent_widget.emit(SIGNAL("pythonpath_changed()")) - - def close_unrelated_projects(self, projects): - """Close unrelated projects""" - unrelated_projects = self.workspace.close_unrelated_projects(projects) - if unrelated_projects: - self.reset_icon_provider() - for project in unrelated_projects: - self.update(self.get_index(project.root_path)) - - def is_valid_project_root_path(self, root_path, silent=False): - """Return True root_path is a valid project root path""" - fixed_wr = fixpath(self.root_path) # workspace root path - fixed_pr = fixpath(osp.dirname(root_path)) # project root path - if self.workspace.has_project(root_path): - if not silent: - QMessageBox.critical(self, _("Project Explorer"), - _("The project %s" - " is already opened!" - ) % osp.basename(root_path)) - return False - elif fixed_pr != fixed_wr and fixed_pr.startswith(fixed_wr): - if not silent: - QMessageBox.warning(self, _("Project Explorer"), - _("The project root path directory " - "is inside the workspace but not as the " - "expected tree level. It is not a " - "directory of the workspace:
    " - "%s") % self.get_workspace()) - return True - - def _select_project_name(self, title, default=None): - """Select project name""" - name = '' if default is None else default - while True: - name, valid = QInputDialog.getText(self, title, _('Project name:'), - QLineEdit.Normal, name) - if valid and name: - name = to_text_string(name) - pattern = r'[a-zA-Z][a-zA-Z0-9\_\-]*$' - match = re.match(pattern, name) - path = self.get_project_path_from_name(name) - if self.workspace.has_project(name): - QMessageBox.critical(self, title, - _("A project named " - "%s already exists") % name) - continue - elif match is None: - QMessageBox.critical(self, title, - _("Invalid project name.

    " - "Name must match the following " - "regular expression:" - "
    %s") % pattern) - continue - elif osp.isdir(path): - answer = QMessageBox.warning(self, title, - _("The following directory is not empty:" - "
    %s

    " - "Do you want to continue?") % path, - QMessageBox.Yes|QMessageBox.No) - if answer == QMessageBox.No: - continue - return name - else: - return - - def new_project(self): - """Return True if project was created""" - title = _('New project') - if self.workspace.is_valid(): - name = self._select_project_name(title) - if name is not None: - folder = self.get_project_path_from_name(name) - self.add_project(folder) - else: - answer = QMessageBox.critical(self, title, - _("The current workspace has " - "not been configured yet.\n" - "Do you want to do this now?"), - QMessageBox.Yes|QMessageBox.Cancel) - if answer == QMessageBox.Yes: - self.emit(SIGNAL('select_workspace()')) - - def _select_existing_directory(self): - """Select existing source code directory, - to be used as a new project root path - (copied into the current project's workspace directory if necessary)""" - if self.last_folder is None: - self.last_folder = self.workspace.root_path - while True: - self.parent_widget.emit(SIGNAL('redirect_stdio(bool)'), False) - folder = getexistingdirectory(self, _("Select directory"), - self.last_folder) - self.parent_widget.emit(SIGNAL('redirect_stdio(bool)'), True) - if folder: - folder = osp.abspath(folder) - self.last_folder = folder - if not self.is_valid_project_root_path(folder): - continue - return folder - else: - return - - def import_existing_directory(self): - """Create project from existing directory - Eventually copy the whole directory to current workspace""" - folder = self._select_existing_directory() - if folder is None: - return - self.add_project(folder) - - def __select_existing_project(self, typename, configname): - """Select existing project""" - title = _('Import existing project') - while True: - folder = self._select_existing_directory() - if folder is None: - return - if not osp.isfile(osp.join(folder, configname)): - subfolders = [osp.join(folder, _f) for _f in os.listdir(folder) - if osp.isdir(osp.join(folder, _f)) - and osp.isfile(osp.join(folder, _f, configname))] - if subfolders: - data = [] - for subfolder in subfolders: - data.append((subfolder, False)) - comment = _("Select projects to import") - result = fedit(data, title=title, comment=comment) - if result is None: - return - else: - selected_folders = [] - for index, is_selected in enumerate(result): - if is_selected: - selected_folders.append(subfolders[index]) - return selected_folders - else: - QMessageBox.critical(self, title, - _("The folder %s " - "does not contain a valid %s project" - ) % (osp.basename(folder), typename)) - continue - return folder - - def import_existing_project(self): - """Import existing project""" - folders = self.__select_existing_project("Spyder", Project.CONFIG_NAME) - if folders is None: - return - if not isinstance(folders, (tuple, list)): - folders = [folders] - for folder in folders: - self.add_project(folder, silent=True) - - def import_existing_pydev_project(self): - """Import existing Pydev project""" - folders = self.__select_existing_project("Pydev", ".pydevproject") - if folders is None: - return - if not isinstance(folders, (tuple, list)): - folders = [folders] - for folder in folders: - try: - name, related_projects, path = get_pydev_project_infos(folder) - except RuntimeError as error: - QMessageBox.critical(self, - _('Import existing Pydev project'), - _("Unable to read Pydev project %s" - "

    Error message:
    %s" - ) % (osp.basename(folder), - to_text_string(error))) - finally: - project = self.add_project(folder, silent=True) - if project is not None: - project.name = name - project.set_related_projects(related_projects) - project.set_pythonpath(path) - self.parent_widget.emit(SIGNAL("pythonpath_changed()")) - - def rename_file(self, fname): - """Rename file""" - path = FilteredDirView.rename_file(self, fname) - if path: - project = self.get_source_project(fname) - if project.is_root_path(fname): - self.workspace.rename_project(project, osp.basename(path)) - self.set_folder_names(self.workspace.get_folder_names()) - else: - self.remove_path_from_project_pythonpath(project, fname) - - def remove_tree(self, dirname): - """Remove whole directory tree""" - FilteredDirView.remove_tree(self, dirname) - project = self.get_source_project(dirname) - self.remove_path_from_project_pythonpath(project, dirname) - - def delete_file(self, fname, multiple, yes_to_all): - """Delete file""" - if multiple: - pj_buttons = QMessageBox.Yes|QMessageBox.No|QMessageBox.Cancel - else: - pj_buttons = QMessageBox.Yes|QMessageBox.No - project = self.get_source_project(fname) - if project.is_root_path(fname): - answer = QMessageBox.warning(self, _("Delete"), - _("Do you really want " - "to delete project %s?

    " - "Note: project files won't be deleted from " - "disk.") % project.name, pj_buttons) - if answer == QMessageBox.Yes: - self.remove_projects([project]) - return yes_to_all - else: - return FilteredDirView.delete_file(self, fname, multiple, - yes_to_all) - - def add_to_path(self, fnames): - """Add fnames to path""" - indexes = [] - for path in fnames: - project = self.get_source_project(path) - if project.add_to_pythonpath(path): - self.parent_widget.emit(SIGNAL("pythonpath_changed()")) - indexes.append(self.get_index(path)) - if indexes: - self.reset_icon_provider() - for index in indexes: - self.update(index) - - def remove_path_from_project_pythonpath(self, project, path): - """Remove path from project's PYTHONPATH""" - ok = project.remove_from_pythonpath(path) - self.parent_widget.emit(SIGNAL("pythonpath_changed()")) - return ok - - def remove_from_path(self, fnames): - """Remove from path""" - indexes = [] - for path in fnames: - project = self.get_source_project(path) - if self.remove_path_from_project_pythonpath(project, path): - indexes.append(self.get_index(path)) - if indexes: - self.reset_icon_provider() - for index in indexes: - self.update(index) - - def manage_path(self, projects): - """Manage path""" - for project in projects: - pathlist = project.get_pythonpath() - dlg = PathManager(self, pathlist, sync=False) - dlg.exec_() - project.set_pythonpath(dlg.get_path_list()) - self.parent_widget.emit(SIGNAL("pythonpath_changed()")) - - def edit_related_projects(self, projects): - """Edit related projects""" - title = _('Related projects') - for project in projects: - related_projects = project.get_related_projects() - data = [] - other_projects = self.workspace.get_other_projects(project) - for proj in other_projects: - name = proj.name - data.append((name, name in related_projects)) - comment = _("Select projects which are related to " - "%s") % project.name - result = fedit(data, title=title, comment=comment) - if result is not None: - related_projects = [] - for index, is_related in enumerate(result): - if is_related: - name = other_projects[index].name - related_projects.append(name) - project.set_related_projects(related_projects) - - def show_properties(self, fnames): - """Show properties""" - pathlist = sorted(fnames) - dirlist = [path for path in pathlist if osp.isdir(path)] - for path in pathlist[:]: - for folder in dirlist: - if path != folder and path.startswith(folder): - pathlist.pop(pathlist.index(path)) - files, lines = 0, 0 - for path in pathlist: - f, l = misc.count_lines(path) - files += f - lines += l - QMessageBox.information(self, _("Project Explorer"), - _("Statistics on source files only:
    " - "(Python, C/C++, Fortran)

    " - "%s files.
    " - "%s lines of code." - ) % (str(files), str(lines))) - - #---- Internal drag & drop - def dragMoveEvent(self, event): - """Reimplement Qt method""" - index = self.indexAt(event.pos()) - if index: - dst = self.get_filename(index) - if osp.isdir(dst): - event.acceptProposedAction() - else: - event.ignore() - else: - event.ignore() - - def dropEvent(self, event): - """Reimplement Qt method""" - event.ignore() - action = event.dropAction() - if action not in (Qt.MoveAction, Qt.CopyAction): - return - -# # QTreeView must not remove the source items even in MoveAction mode: -# event.setDropAction(Qt.CopyAction) - - dst = self.get_filename(self.indexAt(event.pos())) - yes_to_all, no_to_all = None, None - src_list = [to_text_string(url.toString()) - for url in event.mimeData().urls()] - if len(src_list) > 1: - buttons = QMessageBox.Yes|QMessageBox.YesAll| \ - QMessageBox.No|QMessageBox.NoAll|QMessageBox.Cancel - else: - buttons = QMessageBox.Yes|QMessageBox.No - for src in src_list: - if src == dst: - continue - dst_fname = osp.join(dst, osp.basename(src)) - if osp.exists(dst_fname): - if yes_to_all is not None or no_to_all is not None: - if no_to_all: - continue - elif osp.isfile(dst_fname): - answer = QMessageBox.warning(self, _('Project explorer'), - _('File %s already exists.
    ' - 'Do you want to overwrite it?') % dst_fname, - buttons) - if answer == QMessageBox.No: - continue - elif answer == QMessageBox.Cancel: - break - elif answer == QMessageBox.YesAll: - yes_to_all = True - elif answer == QMessageBox.NoAll: - no_to_all = True - continue - else: - QMessageBox.critical(self, _('Project explorer'), - _('Folder %s already exists.' - ) % dst_fname, QMessageBox.Ok) - event.setDropAction(Qt.CopyAction) - return - try: - if action == Qt.CopyAction: - if osp.isfile(src): - shutil.copy(src, dst) - else: - shutil.copytree(src, dst) - else: - if osp.isfile(src): - misc.move_file(src, dst) - else: - shutil.move(src, dst) - self.parent_widget.emit(SIGNAL("removed(QString)"), src) - except EnvironmentError as error: - if action == Qt.CopyAction: - action_str = _('copy') - else: - action_str = _('move') - QMessageBox.critical(self, _("Project Explorer"), - _("Unable to %s %s" - "

    Error message:
    %s" - ) % (action_str, src, - to_text_string(error))) - - -class WorkspaceSelector(QWidget): - """Workspace selector widget""" - TITLE = _('Select an existing workspace directory, or create a new one') - TIP = _("What is the workspace?" - "

    " - "A Spyder workspace is a directory on your filesystem that " - "contains Spyder projects and .spyderworkspace configuration " - "file." - "

    " - "A Spyder project is a directory with source code (and other " - "related files) and a configuration file (named " - ".spyderproject) with project settings (PYTHONPATH, linked " - "projects, ...).
    " - ) - - def __init__(self, parent): - super(WorkspaceSelector, self).__init__(parent) - self.browse_btn = None - self.create_btn = None - self.line_edit = None - self.first_time = True - - def set_workspace(self, path): - """Set workspace directory""" - self.line_edit.setText(path) - - def setup_widget(self): - """Setup workspace selector widget""" - self.line_edit = QLineEdit() - self.line_edit.setAlignment(Qt.AlignRight) - self.line_edit.setToolTip(_("This is the current workspace directory")\ - +'

    '+self.TIP) - self.line_edit.setReadOnly(True) - self.line_edit.setDisabled(True) - self.browse_btn = QPushButton(get_std_icon('DirOpenIcon'), "", self) - self.browse_btn.setToolTip(self.TITLE) - self.connect(self.browse_btn, SIGNAL("clicked()"), - self.select_directory) - layout = QHBoxLayout() - layout.addWidget(self.line_edit) - layout.addWidget(self.browse_btn) - layout.setContentsMargins(0, 0, 0, 0) - self.setLayout(layout) - - def select_directory(self): - """Select directory""" - if self.first_time: - QMessageBox.information(self, self.TITLE, self.TIP) - self.first_time = False - basedir = to_text_string(self.line_edit.text()) - if not osp.isdir(basedir): - basedir = getcwd() - while True: - self.parent().emit(SIGNAL('redirect_stdio(bool)'), False) - directory = getexistingdirectory(self, self.TITLE, basedir) - self.parent().emit(SIGNAL('redirect_stdio(bool)'), True) - if not directory: - break - path = osp.join(directory, Workspace.CONFIG_NAME) - if not osp.isfile(path): - answer = QMessageBox.warning(self, self.TITLE, - _("The following directory is not a Spyder " - "workspace:
    %s

    Do you want to " - "create a new workspace in this directory?" - ) % directory, QMessageBox.Yes|QMessageBox.No) - if answer == QMessageBox.No: - continue - directory = osp.abspath(osp.normpath(directory)) - self.set_workspace(directory) - self.emit(SIGNAL('selected_workspace(QString)'), directory) - break - - -class ProjectExplorerWidget(QWidget): - """ - Project Explorer - - Signals: - sig_open_file - SIGNAL("create_module(QString)") - SIGNAL("pythonpath_changed()") - SIGNAL("renamed(QString,QString)") - SIGNAL("removed(QString)") - """ - sig_option_changed = Signal(str, object) - sig_open_file = Signal(str) - - def __init__(self, parent, name_filters=['*.py', '*.pyw'], - show_all=False, show_hscrollbar=True): - QWidget.__init__(self, parent) - self.treewidget = None - self.selector = None - self.setup_layout(name_filters, show_all, show_hscrollbar) - - def setup_layout(self, name_filters, show_all, show_hscrollbar): - """Setup project explorer widget layout""" - self.selector = WorkspaceSelector(self) - self.selector.setup_widget() - self.connect(self.selector, SIGNAL('selected_workspace(QString)'), - self.set_workspace) - - self.treewidget = ExplorerTreeWidget(self, - show_hscrollbar=show_hscrollbar) - self.treewidget.setup(name_filters=name_filters, show_all=show_all) - self.connect(self.treewidget, SIGNAL('select_workspace()'), - self.selector.select_directory) - - layout = QVBoxLayout() - layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(self.selector) - layout.addWidget(self.treewidget) - self.setLayout(layout) - - def set_workspace(self, path): - """Set current workspace""" - path = osp.normpath(to_text_string(path)) - if path is not None and osp.isdir(path): - self.treewidget.set_workspace(path) - self.selector.set_workspace(path) - - def check_for_io_errors(self): - """Check for I/O errors that may occured when loading/saving - projects or the workspace itself and warn the user""" - self.treewidget.check_for_io_errors() - - def get_workspace(self): - """Return current workspace path""" - return self.treewidget.get_workspace() - - def closing_widget(self): - """Perform actions before widget is closed""" - pass - - def add_project(self, project): - """Add project""" - return self.treewidget.add_project(project) - - def get_pythonpath(self): - """Return PYTHONPATH""" - return self.treewidget.get_pythonpath() - - def get_source_project(self, fname): - """Return project which contains source *fname*""" - return self.treewidget.get_source_project(fname) - - -class Test(QWidget): - def __init__(self): - QWidget.__init__(self) - vlayout = QVBoxLayout() - self.setLayout(vlayout) - - self.explorer = ProjectExplorerWidget(None, show_all=True) - self.explorer.set_workspace(r'D:/Python') -# p1 = self.explorer.add_project(r"D:/Python/spyder") -# p1.set_pythonpath([r"D:\Python\spyder\spyderlib"]) -# p1.save() -# self.treewidget.close_projects(p1) -# _p2 = self.explorer.add_project(r"D:\Python\test_project") - - vlayout.addWidget(self.explorer) - - hlayout1 = QHBoxLayout() - vlayout.addLayout(hlayout1) - label = QLabel("Open file:") - label.setAlignment(Qt.AlignRight) - hlayout1.addWidget(label) - self.label1 = QLabel() - hlayout1.addWidget(self.label1) - self.explorer.sig_open_file.connect(self.label1.setText) - - hlayout3 = QHBoxLayout() - vlayout.addLayout(hlayout3) - label = QLabel("Option changed:") - label.setAlignment(Qt.AlignRight) - hlayout3.addWidget(label) - self.label3 = QLabel() - hlayout3.addWidget(self.label3) - self.explorer.sig_option_changed.connect( - lambda x, y: self.label3.setText('option_changed: %r, %r' % (x, y))) - - -if __name__ == "__main__": - from spyderlib.utils.qthelpers import qapplication - app = qapplication() - test = Test() - test.resize(640, 480) - test.show() - app.exec_() diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/widgets/projects/configdialog.py spyder-3.0.2+dfsg1/spyder/widgets/projects/configdialog.py --- spyder-2.3.8+dfsg1/spyder/widgets/projects/configdialog.py 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/widgets/projects/configdialog.py 2016-10-25 02:05:23.000000000 +0200 @@ -0,0 +1,190 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright © Spyder Project Contributors +# +# Licensed under the terms of the MIT License +# (see spyder/__init__.py for details) +# ----------------------------------------------------------------------------- +"""Configuration dialog for projects""" + +from qtpy.QtWidgets import QGroupBox, QVBoxLayout + +from spyder.config.base import _ +from spyder.plugins.configdialog import ConfigDialog, GeneralConfigPage +from spyder.utils.qthelpers import get_icon +from spyder.config.user import NoDefault +from spyder.widgets.projects import EmptyProject +from spyder.widgets.projects.config import (WORKSPACE, VCS, ENCODING, + CODESTYLE) + + +class ProjectPreferences(ConfigDialog): + """ """ + def __init__(self, parent, project): + super(ProjectPreferences, self).__init__() + + self._main = parent + self._project = project + self._project_preferences = [WorkspaceConfigPage] #, VersionConfigPage] + + self.setWindowTitle(_("Project preferences")) + self.setWindowIcon(get_icon("configure.png")) + + self.setup_dialog() + + def setup_dialog(self): + """ """ + # Move to spyder.py +# dlg = ConfigDialog(self) +# dlg.size_change.connect(self.set_prefs_size) +# if self.prefs_dialog_size is not None: +# dlg.resize(self.prefs_dialog_size) + for PrefPageClass in self._project_preferences: + widget = PrefPageClass(self, self._main, self._project) + widget.initialize() + self.add_page(widget) + + +class ProjectConfigPage(GeneralConfigPage): + """General config page that redefines the configuration accessors.""" + CONF_SECTION = None + NAME = None + ICON = None + + def __init__(self, parent, main, project): + self._project = project + self._conf_files = project.get_conf_files() + self._conf = self._conf_files[self.CONF_SECTION] + + GeneralConfigPage.__init__(self, parent, main) + + def set_option(self, option, value): + """ """ + CONF = self._conf + CONF.set(self.CONF_SECTION, option, value) + + def get_option(self, option, default=NoDefault): + """" """ + CONF = self._conf + return CONF.get(self.CONF_SECTION, option, default) + + +class WorkspaceConfigPage(ProjectConfigPage): + CONF_SECTION = WORKSPACE + NAME = _("General") + ICON = "genprefs.png" + + def setup_page(self): + newcb = self.create_checkbox + + # --- Workspace + interface_group = QGroupBox(_("Interface")) + restore_data_box = newcb(_("Restore data on startup"), + 'restore_data_on_startup') + save_data_box = newcb(_("Save data on exit"), + 'save_data_on_exit') + save_history_box = newcb(_("Save history"), + 'save_history') + save_non_project_box = newcb(_("Save non project files opened"), + 'save_non_project_files') + + interface_layout = QVBoxLayout() + interface_layout.addWidget(restore_data_box) + interface_layout.addWidget(save_data_box) + interface_layout.addWidget(save_history_box) + interface_layout.addWidget(save_non_project_box) + interface_group.setLayout(interface_layout) + + vlayout = QVBoxLayout() + vlayout.addWidget(interface_group) + vlayout.addStretch(1) + self.setLayout(vlayout) + + def apply_settings(self, options): + """ """ + pass # TODO: + #self.main.apply_settings() + + +class CodeConfigPage(ProjectConfigPage): + CONF_SECTION = CODESTYLE + NAME = _("Code") + ICON = "genprefs.png" + + def setup_page(self): + newcb = self.create_checkbox + + # --- Workspace + interface_group = QGroupBox(_("Workspace")) + restore_data_box = newcb(_("Restore data on startup"), + 'restore_data_on_startup') + save_data_box = newcb(_("Save data on exit"), + 'save_data_on_exit') + save_history_box = newcb(_("Save history"), + 'save_history') + save_non_project_box = newcb(_("Save non project files opened"), + 'save_non_project_files') + + interface_layout = QVBoxLayout() + interface_layout.addWidget(restore_data_box) + interface_layout.addWidget(save_data_box) + interface_layout.addWidget(save_history_box) + interface_layout.addWidget(save_non_project_box) + interface_group.setLayout(interface_layout) + + vlayout = QVBoxLayout() + vlayout.addWidget(interface_group) + vlayout.addStretch(1) + self.setLayout(vlayout) + + def apply_settings(self, options): + """ """ + print('applied') + #self.main.apply_settings() + + +class VersionConfigPage(ProjectConfigPage): + CONF_SECTION = VCS + NAME = _("Version control") + ICON = "genprefs.png" + + def setup_page(self): + newcb = self.create_checkbox + + # --- Workspace + vcs_group = QGroupBox(_("Version control")) + use_version_control = newcb(_("Use version control"), + 'use_version_control') + + styles = ['git', 'hg'] + choices = list(zip(styles, [style.lower() for style in styles])) + vcs_combo = self.create_combobox(_('Version control system'), choices, + 'version_control_system', + default='git') + + vcs_layout = QVBoxLayout() + vcs_layout.addWidget(use_version_control) + vcs_layout.addWidget(vcs_combo) + vcs_group.setLayout(vcs_layout) + + vlayout = QVBoxLayout() + vlayout.addWidget(vcs_group) + vlayout.addStretch(1) + self.setLayout(vlayout) + + def apply_settings(self, options): + """ """ + print('applied') + #self.main.apply_settings() + + +if __name__ == "__main__": + import os.path as osp + import tempfile + from spyder.utils.qthelpers import qapplication + app = qapplication() + proj_dir = tempfile.mkdtemp() + osp.sep + '.spyproject' + proj = EmptyProject(proj_dir) + dlg = ProjectPreferences(None, proj) + dlg.show() + app.exec_() diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/widgets/projects/config.py spyder-3.0.2+dfsg1/spyder/widgets/projects/config.py --- spyder-2.3.8+dfsg1/spyder/widgets/projects/config.py 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/widgets/projects/config.py 2016-10-25 02:05:23.000000000 +0200 @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright © Spyder Project Contributors +# +# Licensed under the terms of the MIT License +# (see spyder/__init__.py for details) +# ----------------------------------------------------------------------------- +"""Configuration options for projects""" + +# Standard library imports +import os + +# Local imports +from spyder.config.user import UserConfig + +PROJECT_FILENAME = '.spyproj' +PROJECT_FOLDER = '.spyproject' + + +# Project configuration defaults +WORKSPACE = 'workspace' +WORKSPACE_DEFAULTS = [ + (WORKSPACE, + {'restore_data_on_startup': True, + 'save_data_on_exit': True, + 'save_history': True, + 'save_non_project_files': False, + } + )] +WORKSPACE_VERSION = '0.1.0' + + +CODESTYLE = 'codestyle' +CODESTYLE_DEFAULTS = [ + (CODESTYLE, + {'indentation': True, + } + )] +CODESTYLE_VERSION = '0.1.0' + + +ENCODING = 'encoding' +ENCODING_DEFAULTS = [ + (ENCODING, + {'text_encoding': 'utf-8', + } + )] +ENCODING_VERSION = '0.1.0' + + +VCS = 'vcs' +VCS_DEFAULTS = [ + (VCS, + {'use_version_control': False, + 'version_control_system': '', + } + )] +VCS_VERSION = '0.1.0' + + +class ProjectConfig(UserConfig): + """ProjectConfig class, based on UserConfig. + + Parameters + ---------- + name: str + name of the config + defaults: tuple + dictionnary containing options *or* list of tuples + (section_name, options) + version: str + version of the configuration file (X.Y.Z format) + filename: str + configuration file will be saved in %home%/subfolder/%name%.ini + """ + DEFAULT_SECTION_NAME = 'main' + + def __init__(self, name, root_path, filename, defaults=None, load=True, + version=None): + self.project_root_path = root_path + + # Config rootpath + self._root_path = os.path.join(root_path, PROJECT_FOLDER) + self._filename = filename + + # Create folder if non existent + if not os.path.isdir(self._root_path): + os.makedirs(self._root_path) + + # Add file + # NOTE: We have to think better about the uses of this file + # with open(os.path.join(root_path, PROJECT_FILENAME), 'w') as f: + # f.write('spyder-ide project\n') + + UserConfig.__init__(self, name, defaults=defaults, load=load, + version=version, subfolder=None, backup=False, + raw_mode=True, remove_obsolete=True) + diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/widgets/projects/explorer.py spyder-3.0.2+dfsg1/spyder/widgets/projects/explorer.py --- spyder-2.3.8+dfsg1/spyder/widgets/projects/explorer.py 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/widgets/projects/explorer.py 2016-11-17 04:39:40.000000000 +0100 @@ -0,0 +1,304 @@ +# -*- coding: utf-8 -*- +# +# Copyright © Spyder Project Contributors +# Licensed under the terms of the MIT License +# (see spyder/__init__.py for details) + +"""Project Explorer""" + +# pylint: disable=C0103 + +# Standard library imports +from __future__ import print_function + +import os.path as osp +import shutil + +# Third party imports +from qtpy import PYQT5 +from qtpy.QtCore import Qt, Signal, Slot +from qtpy.QtWidgets import (QAbstractItemView, QHBoxLayout, QHeaderView, + QLabel, QMessageBox, QVBoxLayout, QWidget) + +# Local imports +from spyder.config.base import _ +from spyder.py3compat import to_text_string +from spyder.utils import misc +from spyder.utils.qthelpers import create_action +from spyder.widgets.explorer import FilteredDirView + + +class ExplorerTreeWidget(FilteredDirView): + """Explorer tree widget""" + + sig_delete_project = Signal() + + def __init__(self, parent, show_hscrollbar=True): + FilteredDirView.__init__(self, parent) + self.last_folder = None + self.setSelectionMode(FilteredDirView.ExtendedSelection) + self.show_hscrollbar = show_hscrollbar + + # Enable drag & drop events + self.setDragEnabled(True) + self.setDragDropMode(FilteredDirView.DragDrop) + + #------DirView API--------------------------------------------------------- + def setup_common_actions(self): + """Setup context menu common actions""" + actions = FilteredDirView.setup_common_actions(self) + + # Toggle horizontal scrollbar + hscrollbar_action = create_action(self, _("Show horizontal scrollbar"), + toggled=self.toggle_hscrollbar) + hscrollbar_action.setChecked(self.show_hscrollbar) + self.toggle_hscrollbar(self.show_hscrollbar) + + return actions + [hscrollbar_action] + + #------Public API---------------------------------------------------------- + @Slot(bool) + def toggle_hscrollbar(self, checked): + """Toggle horizontal scrollbar""" + self.parent_widget.sig_option_changed.emit('show_hscrollbar', checked) + self.show_hscrollbar = checked + self.header().setStretchLastSection(not checked) + self.header().setHorizontalScrollMode(QAbstractItemView.ScrollPerPixel) + if PYQT5: + self.header().setSectionResizeMode(QHeaderView.ResizeToContents) + else: + self.header().setResizeMode(QHeaderView.ResizeToContents) + + #---- Internal drag & drop + def dragMoveEvent(self, event): + """Reimplement Qt method""" + index = self.indexAt(event.pos()) + if index: + dst = self.get_filename(index) + if osp.isdir(dst): + event.acceptProposedAction() + else: + event.ignore() + else: + event.ignore() + + def dropEvent(self, event): + """Reimplement Qt method""" + event.ignore() + action = event.dropAction() + if action not in (Qt.MoveAction, Qt.CopyAction): + return + + # QTreeView must not remove the source items even in MoveAction mode: + # event.setDropAction(Qt.CopyAction) + + dst = self.get_filename(self.indexAt(event.pos())) + yes_to_all, no_to_all = None, None + src_list = [to_text_string(url.toString()) + for url in event.mimeData().urls()] + if len(src_list) > 1: + buttons = QMessageBox.Yes|QMessageBox.YesAll| \ + QMessageBox.No|QMessageBox.NoAll|QMessageBox.Cancel + else: + buttons = QMessageBox.Yes|QMessageBox.No + for src in src_list: + if src == dst: + continue + dst_fname = osp.join(dst, osp.basename(src)) + if osp.exists(dst_fname): + if yes_to_all is not None or no_to_all is not None: + if no_to_all: + continue + elif osp.isfile(dst_fname): + answer = QMessageBox.warning(self, _('Project explorer'), + _('File %s already exists.
    ' + 'Do you want to overwrite it?') % dst_fname, + buttons) + if answer == QMessageBox.No: + continue + elif answer == QMessageBox.Cancel: + break + elif answer == QMessageBox.YesAll: + yes_to_all = True + elif answer == QMessageBox.NoAll: + no_to_all = True + continue + else: + QMessageBox.critical(self, _('Project explorer'), + _('Folder %s already exists.' + ) % dst_fname, QMessageBox.Ok) + event.setDropAction(Qt.CopyAction) + return + try: + if action == Qt.CopyAction: + if osp.isfile(src): + shutil.copy(src, dst) + else: + shutil.copytree(src, dst) + else: + if osp.isfile(src): + misc.move_file(src, dst) + else: + shutil.move(src, dst) + self.parent_widget.removed.emit(src) + except EnvironmentError as error: + if action == Qt.CopyAction: + action_str = _('copy') + else: + action_str = _('move') + QMessageBox.critical(self, _("Project Explorer"), + _("Unable to %s %s" + "

    Error message:
    %s" + ) % (action_str, src, + to_text_string(error))) + @Slot() + def delete(self, fnames=None): + """Delete files""" + if fnames is None: + fnames = self.get_selected_filenames() + multiple = len(fnames) > 1 + yes_to_all = None + for fname in fnames: + if fname == self.proxymodel.path_list[0]: + self.sig_delete_project.emit() + else: + yes_to_all = self.delete_file(fname, multiple, yes_to_all) + if yes_to_all is not None and not yes_to_all: + # Canceled + break + + +class ProjectExplorerWidget(QWidget): + """Project Explorer""" + sig_option_changed = Signal(str, object) + sig_open_file = Signal(str) + + def __init__(self, parent, name_filters=[], + show_all=True, show_hscrollbar=True): + QWidget.__init__(self, parent) + self.treewidget = None + self.emptywidget = None + self.name_filters = name_filters + self.show_all = show_all + self.show_hscrollbar = show_hscrollbar + self.setup_layout() + + def setup_layout(self): + """Setup project explorer widget layout""" + + self.emptywidget = ExplorerTreeWidget(self) + + layout = QVBoxLayout() + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(self.emptywidget) + self.setLayout(layout) + + def closing_widget(self): + """Perform actions before widget is closed""" + pass + + def set_project_dir(self, directory): + """Set the project directory""" + if directory is not None: + project = directory.split(osp.sep)[-1] + self.treewidget.set_root_path(osp.dirname(directory)) + self.treewidget.set_folder_names([project]) + self.treewidget.setup_project_view() + try: + self.treewidget.setExpanded(self.treewidget.get_index(directory), + True) + except TypeError: + pass + + def clear(self): + """Show an empty view""" + self.treewidget.hide() + self.emptywidget.show() + + def setup_project(self, directory): + """Setup project""" + if self.treewidget is not None: + self.treewidget.hide() + + # Setup a new tree widget + self.treewidget = ExplorerTreeWidget(self, self.show_hscrollbar) + self.treewidget.setup(name_filters=self.name_filters, + show_all=self.show_all) + self.treewidget.setup_view() + self.emptywidget.hide() + self.treewidget.show() + self.layout().addWidget(self.treewidget) + + # Setup the directory shown by the tree + self.set_project_dir(directory) + + # Signal to delete the project + self.treewidget.sig_delete_project.connect(self.delete_project) + + def delete_project(self): + """Delete current project without deleting the files in the directory.""" + if self.current_active_project: + path = self.current_active_project.root_path + buttons = QMessageBox.Yes|QMessageBox.No + answer = QMessageBox.warning(self, _("Delete"), + _("Do you really want " + "to delete {filename}?

    " + "Note: This action will only delete " + "the project. Its files are going to be " + "preserved on disk." + ).format(filename=osp.basename(path)), + buttons) + if answer == QMessageBox.Yes: + try: + self.close_project() + shutil.rmtree(osp.join(path,'.spyproject')) + except EnvironmentError as error: + QMessageBox.critical(self, _("Project Explorer"), + _("Unable to delete {varpath}" + "

    The error message was:
    {error}" ) + .format(varpath=path,error=to_text_string(error))) + +#============================================================================== +# Tests +#============================================================================== +class Test(QWidget): + def __init__(self): + QWidget.__init__(self) + vlayout = QVBoxLayout() + self.setLayout(vlayout) + + self.explorer = ProjectExplorerWidget(None, show_all=True) + self.explorer.setup_project(osp.dirname(osp.abspath(__file__))) + vlayout.addWidget(self.explorer) + + hlayout1 = QHBoxLayout() + vlayout.addLayout(hlayout1) + label = QLabel("Open file:") + label.setAlignment(Qt.AlignRight) + hlayout1.addWidget(label) + self.label1 = QLabel() + hlayout1.addWidget(self.label1) + self.explorer.sig_open_file.connect(self.label1.setText) + + hlayout3 = QHBoxLayout() + vlayout.addLayout(hlayout3) + label = QLabel("Option changed:") + label.setAlignment(Qt.AlignRight) + hlayout3.addWidget(label) + self.label3 = QLabel() + hlayout3.addWidget(self.label3) + self.explorer.sig_option_changed.connect( + lambda x, y: self.label3.setText('option_changed: %r, %r' % (x, y))) + + +def test(): + from spyder.utils.qthelpers import qapplication + app = qapplication() + test = Test() + test.resize(250, 480) + test.show() + app.exec_() + + +if __name__ == "__main__": + test() diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/widgets/projects/__init__.py spyder-3.0.2+dfsg1/spyder/widgets/projects/__init__.py --- spyder-2.3.8+dfsg1/spyder/widgets/projects/__init__.py 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/widgets/projects/__init__.py 2016-10-25 02:05:23.000000000 +0200 @@ -0,0 +1,96 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright © Spyder Project Contributors +# +# Licensed under the terms of the MIT License +# (see spyder/__init__.py for details) +# ----------------------------------------------------------------------------- +"""Projects""" + +# Local imports +from spyder.widgets.projects.type import EmptyProject +from spyder.widgets.projects.type.python import PythonProject + + +def get_available_project_types(): + """ """ +# return [EmptyProject, PythonProject, PythonPackageProject, DjangoProject, +# SpyderPluginProject] + get_available_project_types_plugins() + return ([EmptyProject] + + get_available_project_types_plugins()) + + +def get_available_project_types_plugins(): + """ """ + return [] + + +#def components(path): +# ''' +# Returns the individual components of the given file path +# string (for the local operating system). +# +# The returned components, when joined with os.path.join(), point to +# the same location as the original path. +# ''' +# components = [] +# # The loop guarantees that the returned components can be +# # os.path.joined with the path separator and point to the same +# # location: +# while True: +# (new_path, tail) = osp.split(path) # Works on any platform +# components.append(tail) +# if new_path == path: # Root (including drive, on Windows) reached +# break +# path = new_path +# components.append(new_path) +# +# components.reverse() # First component first +# return components +# +# +#def longest_prefix(iter0, iter1): +# ''' +# Returns the longest common prefix of the given two iterables. +# ''' +# longest_prefix = [] +# for (elmt0, elmt1) in itertools.izip(iter0, iter1): +# if elmt0 != elmt1: +# break +# longest_prefix.append(elmt0) +# return longest_prefix +# +# +#def common_prefix_path(path0, path1): +# return os.path.join(*longest_prefix(components(path0), components(path1))) +# + +# +#def has_children_files(path, include, exclude, show_all): +# """Return True if path has children files""" +# try: +# return len(listdir(path, include, exclude, show_all)) > 0 +# except (IOError, OSError): +# return False +# +# +#def is_drive_path(path): +# """Return True if path is a drive (Windows)""" +# path = osp.abspath(path) +# return osp.normpath(osp.join(path, osp.pardir)) == path +# +# +#def get_dir_icon(dirname, project): +# """Return appropriate directory icon""" +# if is_drive_path(dirname): +# return get_std_icon('DriveHDIcon') +# prefix = 'pp_' if dirname in project.get_pythonpath() else '' +# if dirname == project.root_path: +# if project.is_opened(): +# return get_icon(prefix+'project.png') +# else: +# return get_icon('project_closed.png') +# elif osp.isfile(osp.join(dirname, '__init__.py')): +# return get_icon(prefix+'package.png') +# else: +# return get_icon(prefix+'folder.png') diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/widgets/projects/projectdialog.py spyder-3.0.2+dfsg1/spyder/widgets/projects/projectdialog.py --- spyder-2.3.8+dfsg1/spyder/widgets/projects/projectdialog.py 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/widgets/projects/projectdialog.py 2016-10-25 02:05:23.000000000 +0200 @@ -0,0 +1,199 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright © Spyder Project Contributors +# +# Licensed under the terms of the MIT License +# (see spyder/__init__.py for details) +# ----------------------------------------------------------------------------- +"""Project creation dialog.""" + +from __future__ import print_function + +# Standard library imports +import errno +import os +import os.path as osp +import sys +import tempfile + +# Third party imports +from qtpy.compat import getexistingdirectory +from qtpy.QtCore import Qt, Signal +from qtpy.QtWidgets import (QVBoxLayout, QLabel, QLineEdit, QPushButton, + QDialog, QComboBox, QGridLayout, QToolButton, + QDialogButtonBox, QGroupBox, QRadioButton, + QHBoxLayout) + + +# Local imports +from spyder.config.base import _, get_home_dir +from spyder.utils.qthelpers import get_std_icon +from spyder.py3compat import to_text_string +from spyder.widgets.projects import get_available_project_types + + +def is_writable(path): + """Check if path has write access""" + try: + testfile = tempfile.TemporaryFile(dir=path) + testfile.close() + except OSError as e: + if e.errno == errno.EACCES: # 13 + return False + return True + + +class ProjectDialog(QDialog): + """Project creation dialog.""" + + # path, type, packages + sig_project_creation_requested = Signal(object, object, object) + + def __init__(self, parent): + """Project creation dialog.""" + super(ProjectDialog, self).__init__(parent=parent) + + # Variables + current_python_version = '.'.join([to_text_string(sys.version_info[0]), + to_text_string(sys.version_info[1])]) + python_versions = ['2.7', '3.4', '3.5'] + if current_python_version not in python_versions: + python_versions.append(current_python_version) + python_versions = sorted(python_versions) + + self.project_name = None + self.location = get_home_dir() + + # Widgets + self.groupbox = QGroupBox() + self.radio_new_dir = QRadioButton(_("New directory")) + self.radio_from_dir = QRadioButton(_("Existing directory")) + + self.label_project_name = QLabel(_('Project name')) + self.label_location = QLabel(_('Location')) + self.label_project_type = QLabel(_('Project type')) + self.label_python_version = QLabel(_('Python version')) + + self.text_project_name = QLineEdit() + self.text_location = QLineEdit(get_home_dir()) + self.combo_project_type = QComboBox() + self.combo_python_version = QComboBox() + + self.button_select_location = QToolButton() + self.button_cancel = QPushButton(_('Cancel')) + self.button_create = QPushButton(_('Create')) + + self.bbox = QDialogButtonBox(Qt.Horizontal) + self.bbox.addButton(self.button_cancel, QDialogButtonBox.ActionRole) + self.bbox.addButton(self.button_create, QDialogButtonBox.ActionRole) + + # Widget setup + self.combo_python_version.addItems(python_versions) + self.radio_new_dir.setChecked(True) + self.text_location.setEnabled(True) + self.text_location.setReadOnly(True) + self.button_select_location.setIcon(get_std_icon('DirOpenIcon')) + self.button_cancel.setDefault(True) + self.button_cancel.setAutoDefault(True) + self.button_create.setEnabled(False) + self.combo_project_type.addItems(self._get_project_types()) + self.combo_python_version.setCurrentIndex( + python_versions.index(current_python_version)) + self.setWindowTitle(_('Create new project')) + self.setFixedWidth(500) + self.label_python_version.setVisible(False) + self.combo_python_version.setVisible(False) + + # Layouts + layout_top = QHBoxLayout() + layout_top.addWidget(self.radio_new_dir) + layout_top.addWidget(self.radio_from_dir) + layout_top.addStretch(1) + self.groupbox.setLayout(layout_top) + + layout_grid = QGridLayout() + layout_grid.addWidget(self.label_project_name, 0, 0) + layout_grid.addWidget(self.text_project_name, 0, 1, 1, 2) + layout_grid.addWidget(self.label_location, 1, 0) + layout_grid.addWidget(self.text_location, 1, 1) + layout_grid.addWidget(self.button_select_location, 1, 2) + layout_grid.addWidget(self.label_project_type, 2, 0) + layout_grid.addWidget(self.combo_project_type, 2, 1, 1, 2) + layout_grid.addWidget(self.label_python_version, 3, 0) + layout_grid.addWidget(self.combo_python_version, 3, 1, 1, 2) + + layout = QVBoxLayout() + layout.addWidget(self.groupbox) + layout.addSpacing(10) + layout.addLayout(layout_grid) + layout.addStretch() + layout.addSpacing(20) + layout.addWidget(self.bbox) + + self.setLayout(layout) + + # Signals and slots + self.button_select_location.clicked.connect(self.select_location) + self.button_create.clicked.connect(self.create_project) + self.button_cancel.clicked.connect(self.close) + self.radio_from_dir.clicked.connect(self.update_location) + self.radio_new_dir.clicked.connect(self.update_location) + self.text_project_name.textChanged.connect(self.update_location) + + def _get_project_types(self): + """Get all available project types.""" + project_types = get_available_project_types() + projects = [] + + for project in project_types: + projects.append(project.PROJECT_TYPE_NAME) + + return projects + + def select_location(self): + """Select directory.""" + location = getexistingdirectory(self, _("Select directory"), + self.location) + if location: + if is_writable(location): + self.location = location + self.update_location() + + def update_location(self, text=''): + """Update text of location.""" + self.text_project_name.setEnabled(self.radio_new_dir.isChecked()) + name = self.text_project_name.text().strip() + + if name and self.radio_new_dir.isChecked(): + path = osp.join(self.location, name) + self.button_create.setDisabled(os.path.isdir(path)) + elif self.radio_from_dir.isChecked(): + self.button_create.setEnabled(True) + path = self.location + else: + self.button_create.setEnabled(False) + path = self.location + + self.text_location.setText(path) + + def create_project(self): + """Create project.""" + packages = ['python={0}'.format(self.combo_python_version.currentText())] + self.sig_project_creation_requested.emit( + self.text_location.text(), + self.combo_project_type.currentText(), + packages) + self.accept() + + +def test(): + """Local test.""" + from spyder.utils.qthelpers import qapplication + app = qapplication() + dlg = ProjectDialog(None) + dlg.show() + sys.exit(app.exec_()) + + +if __name__ == "__main__": + test() diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/widgets/projects/type/__init__.py spyder-3.0.2+dfsg1/spyder/widgets/projects/type/__init__.py --- spyder-2.3.8+dfsg1/spyder/widgets/projects/type/__init__.py 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/widgets/projects/type/__init__.py 2016-10-25 02:05:23.000000000 +0200 @@ -0,0 +1,215 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright © Spyder Project Contributors +# +# Licensed under the terms of the MIT License +# (see spyder/__init__.py for details) +# ----------------------------------------------------------------------------- +"""Project types""" + +import os +import os.path as osp + +from spyder.config.base import _ +from spyder.py3compat import to_text_string +from spyder.widgets.projects.config import (ProjectConfig, CODESTYLE, + CODESTYLE_DEFAULTS, + CODESTYLE_VERSION, WORKSPACE, + WORKSPACE_DEFAULTS, + WORKSPACE_VERSION, + ENCODING, ENCODING_DEFAULTS, + ENCODING_VERSION, + VCS, VCS_DEFAULTS, VCS_VERSION) + + +class BaseProject(object): + """Spyder base project. + + This base class must not be used directly, but inherited from. It does not + assume that python is specific to this project. + """ + PROJECT_FOLDER = '.spyderproject' + PROJECT_TYPE_NAME = None + IGNORE_FILE = "" + CONFIG_SETUP = {WORKSPACE: {'filename': '{0}.ini'.format(WORKSPACE), + 'defaults': WORKSPACE_DEFAULTS, + 'version': WORKSPACE_VERSION}, + CODESTYLE: {'filename': '{0}.ini'.format(CODESTYLE), + 'defaults': CODESTYLE_DEFAULTS, + 'version': CODESTYLE_VERSION}, + ENCODING: {'filename': '{0}.ini'.format(ENCODING), + 'defaults': ENCODING_DEFAULTS, + 'version': ENCODING_VERSION}, + VCS: {'filename': '{0}.ini'.format(VCS), + 'defaults': VCS_DEFAULTS, + 'version': VCS_VERSION} + } + + def __init__(self, root_path): + self.name = None + self.root_path = root_path + self.open_project_files = [] + self.open_non_project_files = [] + self.config_files = [] + self.CONF = {} + + # Configuration files + + self.related_projects = [] # storing project path, not project objects +# self.pythonpath = [] + self.opened = True + + self.ioerror_flag = False + self.create_project_config_files() + + # --- Helpers + # ------------------------------------------------------------------------- + def set_recent_files(self, recent_files): + """Set a list of files opened by the project.""" + for recent_file in recent_files[:]: + if not os.path.isfile(recent_file): + recent_files.remove(recent_file) + self.CONF[WORKSPACE].set('main', 'recent_files', + list(set(recent_files))) + + def get_recent_files(self): + """Return a list of files opened by the project.""" + recent_files = self.CONF[WORKSPACE].get('main', 'recent_files', + default=[]) + for recent_file in recent_files[:]: + if not os.path.isfile(recent_file): + recent_files.remove(recent_file) + return list(set(recent_files)) + + def create_project_config_files(self): + """ """ + dic = self.CONFIG_SETUP + for key in dic: + name = key + filename = dic[key]['filename'] + defaults = dic[key]['defaults'] + version = dic[key]['version'] + self.CONF[key] = ProjectConfig(name, self.root_path, filename, + defaults=defaults, load=True, + version=version) + + def get_conf_files(self): + """ """ + return self.CONF + + def add_ignore_lines(self, lines): + """ """ + text = self.IGNORE_FILE + for line in lines: + text += line + self.IGNORE_FILE = text + + def set_root_path(self, root_path): + """Set project root path.""" + if self.name is None: + self.name = osp.basename(root_path) + self.root_path = to_text_string(root_path) + config_path = self.__get_project_config_path() + if osp.exists(config_path): + self.load() + else: + if not osp.isdir(self.root_path): + os.mkdir(self.root_path) + self.save() + + def rename(self, new_name): + """Rename project and rename its root path accordingly.""" + old_name = self.name + self.name = new_name + pypath = self.relative_pythonpath # ?? + self.root_path = self.root_path[:-len(old_name)]+new_name + self.relative_pythonpath = pypath # ?? + self.save() + + def __get_project_config_folder(self): + """Return project configuration folder.""" + return osp.join(self.root_path, self.PROJECT_FOLDER) + + def __get_project_config_path(self): + """Return project configuration path""" + return osp.join(self.root_path, self.CONFIG_NAME) + + def load(self): + """Load project data""" +# fname = self.__get_project_config_path() +# try: +# # Old format (Spyder 2.0-2.1 for Python 2) +# with open(fname, 'U') as fdesc: +# data = pickle.loads(fdesc.read()) +# except (pickle.PickleError, TypeError, UnicodeDecodeError, +# AttributeError): +# try: +# # New format (Spyder >=2.2 for Python 2 and Python 3) +# with open(fname, 'rb') as fdesc: +# data = pickle.loads(fdesc.read()) +# except (IOError, OSError, pickle.PickleError): +# self.ioerror_flag = True +# return + # Compatibilty with old project explorer file format: +# if 'relative_pythonpath' not in data: +# print("Warning: converting old configuration file " +# "for project '%s'" % data['name'], file=STDERR) +# self.pythonpath = data['pythonpath'] +# data['relative_pythonpath'] = self.relative_pythonpath +# for attr in self.CONFIG_ATTR: +# setattr(self, attr, data[attr]) +# self.save() + + def save(self): + """Save project data""" +# data = {} +# for attr in self.PROJECT_ATTR: +# data[attr] = getattr(self, attr) +# try: +# with open(self.__get_project_config_path(), 'wb') as fdesc: +# pickle.dump(data, fdesc, 2) +# except (IOError, OSError): +# self.ioerror_flag = True + +# def delete(self): +# """Delete project""" +# os.remove(self.__get_project_config_path()) +# +# # --- Misc. +# def get_related_projects(self): +# """Return related projects path list""" +# return self.related_projects +# +# def set_related_projects(self, related_projects): +# """Set related projects""" +# self.related_projects = related_projects +# self.save() +# +# def open(self): +# """Open project""" +# self.opened = True +# self.save() +# +# def close(self): +# """Close project""" +# self.opened = False +# self.save() +# +# def is_opened(self): +# """Return True if project is opened""" +# return self.opened +# +# def is_file_in_project(self, fname): +# """Return True if file *fname* is in one of the project subfolders""" +# fixed_root = fixpath(self.root_path) +# return fixpath(fname) == fixed_root or\ +# fixpath(osp.dirname(fname)).startswith(fixed_root) +# +# def is_root_path(self, dirname): +# """Return True if dirname is project's root path""" +# return fixpath(dirname) == fixpath(self.root_path) + + +class EmptyProject(BaseProject): + """Empty Project""" + PROJECT_TYPE_NAME = _('Empty project') diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/widgets/projects/type/python.py spyder-3.0.2+dfsg1/spyder/widgets/projects/type/python.py --- spyder-2.3.8+dfsg1/spyder/widgets/projects/type/python.py 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/widgets/projects/type/python.py 2016-10-25 02:05:23.000000000 +0200 @@ -0,0 +1,88 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright © Spyder Project Contributors +# +# Licensed under the terms of the MIT License +# (see spyder/__init__.py for details) +# ----------------------------------------------------------------------------- +"""Python project type""" + +import os +import os.path as osp + +from spyder.config.base import _ +from spyder.widgets.projects.type import EmptyProject + + +class PythonProject(EmptyProject): + """Python project.""" + + PROJECT_TYPE_NAME = _('Python project') + IGNORE_FILE = """""" + + def _get_relative_pythonpath(self): + """Return PYTHONPATH list as relative paths""" + # Workaround to replace os.path.relpath (new in Python v2.6): + offset = len(self.root_path)+len(os.pathsep) + return [path[offset:] for path in self.pythonpath] + + def _set_relative_pythonpath(self, value): + """Set PYTHONPATH list relative paths""" + self.pythonpath = [osp.abspath(osp.join(self.root_path, path)) + for path in value] + + relative_pythonpath = property(_get_relative_pythonpath, + _set_relative_pythonpath) + + # --- Python Path + def is_in_pythonpath(self, dirname): + """Return True if dirname is in project's PYTHONPATH""" + return fixpath(dirname) in [fixpath(_p) for _p in self.pythonpath] + + def get_pythonpath(self): + """Return a copy of pythonpath attribute""" + return self.pythonpath[:] + + def set_pythonpath(self, pythonpath): + """Set project's PYTHONPATH""" + self.pythonpath = pythonpath + self.save() + + def remove_from_pythonpath(self, path): + """Remove path from project's PYTHONPATH + Return True if path was removed, False if it was not found""" + pathlist = self.get_pythonpath() + if path in pathlist: + pathlist.pop(pathlist.index(path)) + self.set_pythonpath(pathlist) + return True + else: + return False + + def add_to_pythonpath(self, path): + """Add path to project's PYTHONPATH + Return True if path was added, False if it was already there""" + pathlist = self.get_pythonpath() + if path in pathlist: + return False + else: + pathlist.insert(0, path) + self.set_pythonpath(pathlist) + return True + + +class PythonPackageProject(PythonProject): + """ """ + PROJECT_TYPE_NAME = _('Python package') + IGNORE_FILE = """ + """ + STRUCTURE_TEMPATE = { + 'relative_path/test.py': + """ +test + """, + 'other/test.py': + """ +test + """, + } diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/widgets/pydocgui.py spyder-3.0.2+dfsg1/spyder/widgets/pydocgui.py --- spyder-2.3.8+dfsg1/spyder/widgets/pydocgui.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/widgets/pydocgui.py 2016-10-25 02:05:23.000000000 +0200 @@ -1,26 +1,31 @@ # -*- coding: utf-8 -*- # -# Copyright © 2009-2010 Pierre Raybaut +# Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) +# (see spyder/__init__.py for details) """pydoc widget""" -from spyderlib.qt.QtGui import QApplication, QCursor -from spyderlib.qt.QtCore import QThread, QUrl, Qt, SIGNAL - -import sys +# Standard library imports import os.path as osp +import sys + +# Third party imports +from qtpy.QtCore import Qt, QThread, QUrl, Signal +from qtpy.QtGui import QCursor +from qtpy.QtWidgets import QApplication # Local imports -from spyderlib.baseconfig import _ -from spyderlib.widgets.browser import WebBrowser -from spyderlib.utils.misc import select_port -from spyderlib.py3compat import to_text_string, PY3 +from spyder.config.base import _ +from spyder.py3compat import PY3, to_text_string +from spyder.utils.misc import select_port +from spyder.widgets.browser import WebBrowser class PydocServer(QThread): """Pydoc server""" + server_started = Signal() + def __init__(self, port=7464): QThread.__init__(self) self.port = port @@ -38,7 +43,7 @@ def callback(self, server): self.server = server - self.emit(SIGNAL('server_started()')) + self.server_started.emit() def completer(self): self.complete = True @@ -93,12 +98,10 @@ self.port = select_port(default_port=self.DEFAULT_PORT) self.set_home_url('http://localhost:%d/' % self.port) elif self.server.isRunning(): - self.disconnect(self.server, SIGNAL('server_started()'), - self.initialize_continued) + self.server.server_started.disconnect(self.initialize_continued) self.server.quit() self.server = PydocServer(port=self.port) - self.connect(self.server, SIGNAL('server_started()'), - self.initialize_continued) + self.server.server_started.connect(self.initialize_continued) self.server.start() #------ WebBrowser API ----------------------------------------------------- @@ -122,14 +125,15 @@ return osp.splitext(to_text_string(url.path()))[0][1:] -def main(): +def test(): """Run web browser""" - from spyderlib.utils.qthelpers import qapplication - app = qapplication() + from spyder.utils.qthelpers import qapplication + app = qapplication(test_time=8) widget = PydocBrowser(None) widget.show() widget.initialize() sys.exit(app.exec_()) + if __name__ == '__main__': - main() + test() diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/widgets/shell.py spyder-3.0.2+dfsg1/spyder/widgets/shell.py --- spyder-2.3.8+dfsg1/spyder/widgets/shell.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/widgets/shell.py 2016-10-25 02:05:23.000000000 +0200 @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- # -# Copyright © 2009-2010 Pierre Raybaut +# Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) +# (see spyder/__init__.py for details) """Shell widgets: base, python and terminal""" @@ -11,6 +11,7 @@ # pylint: disable=R0911 # pylint: disable=R0201 +# Standard library imports import keyword import locale import os @@ -19,24 +20,26 @@ import sys import time -from spyderlib.qt.QtGui import (QMenu, QApplication, QToolTip, QKeySequence, - QMessageBox, QTextCursor, QTextCharFormat) -from spyderlib.qt.QtCore import (Qt, QCoreApplication, QTimer, SIGNAL, - Property) -from spyderlib.qt.compat import getsavefilename +# Third party imports +from qtpy.compat import getsavefilename +from qtpy.QtCore import Property, QCoreApplication, Qt, QTimer, Signal, Slot +from qtpy.QtGui import QKeySequence, QTextCharFormat, QTextCursor +from qtpy.QtWidgets import QApplication, QMenu, QMessageBox, QToolTip # Local import -from spyderlib.baseconfig import get_conf_path, _, STDERR, DEBUG -from spyderlib.config import CONF -from spyderlib.guiconfig import get_font, create_shortcut, get_shortcut -from spyderlib.utils import encoding -from spyderlib.utils.qthelpers import (keybinding, create_action, add_actions, - restore_keyevent, get_icon) -from spyderlib.widgets.sourcecode.base import ConsoleBaseWidget -from spyderlib.widgets.mixins import (InspectObjectMixin, TracebackLinksMixin, - SaveHistoryMixin) -from spyderlib.py3compat import (is_text_string, to_text_string, builtins, - is_string, PY3) +from spyder.config.base import _, DEBUG, get_conf_path, STDERR +from spyder.config.gui import config_shortcut, get_shortcut, fixed_shortcut +from spyder.config.main import CONF +from spyder.py3compat import (builtins, is_string, is_text_string, + PY3, to_text_string) +from spyder.utils import encoding +from spyder.utils import icon_manager as ima +from spyder.utils.qthelpers import (add_actions, create_action, keybinding, + restore_keyevent) +from spyder.widgets.arraybuilder import SHORTCUT_INLINE, SHORTCUT_TABLE +from spyder.widgets.mixins import (GetHelpMixin, SaveHistoryMixin, + TracebackLinksMixin) +from spyder.widgets.sourcecode.base import ConsoleBaseWidget class ShellBaseWidget(ConsoleBaseWidget, SaveHistoryMixin): @@ -44,6 +47,11 @@ Shell base widget """ + redirect_stdio = Signal(bool) + sig_keyboard_interrupt = Signal() + execute = Signal(str) + append_to_history = Signal(str, str) + def __init__(self, parent, history_filename, profile=False): """ parent : specifies the parent widget @@ -78,19 +86,14 @@ self.__timestamp = 0.0 self.__flushtimer = QTimer(self) self.__flushtimer.setSingleShot(True) - self.connect(self.__flushtimer, SIGNAL('timeout()'), self.flush) + self.__flushtimer.timeout.connect(self.flush) # Give focus to widget self.setFocus() - - # Completion - completion_size = CONF.get('shell_appearance', 'completion/size') - completion_font = get_font('console') - self.completion_widget.setup_appearance(completion_size, - completion_font) + # Cursor width - self.setCursorWidth( CONF.get('shell_appearance', 'cursor/width') ) - + self.setCursorWidth( CONF.get('main', 'cursor/width') ) + def toggle_wrap_mode(self, enable): """Enable/disable wrap mode""" self.set_wrap_mode('character' if enable else None) @@ -113,28 +116,28 @@ self.menu = QMenu(self) self.cut_action = create_action(self, _("Cut"), shortcut=keybinding('Cut'), - icon=get_icon('editcut.png'), + icon=ima.icon('editcut'), triggered=self.cut) self.copy_action = create_action(self, _("Copy"), shortcut=keybinding('Copy'), - icon=get_icon('editcopy.png'), + icon=ima.icon('editcopy'), triggered=self.copy) paste_action = create_action(self, _("Paste"), shortcut=keybinding('Paste'), - icon=get_icon('editpaste.png'), + icon=ima.icon('editpaste'), triggered=self.paste) save_action = create_action(self, _("Save history log..."), - icon=get_icon('filesave.png'), + icon=ima.icon('filesave'), tip=_("Save current history log (i.e. all " "inputs and outputs) in a text file"), triggered=self.save_historylog) self.delete_action = create_action(self, _("Delete"), shortcut=keybinding('Delete'), - icon=get_icon('editdelete.png'), + icon=ima.icon('editdelete'), triggered=self.delete) selectall_action = create_action(self, _("Select All"), shortcut=keybinding('SelectAll'), - icon=get_icon('selectall.png'), + icon=ima.icon('selectall'), triggered=self.selectAll) add_actions(self.menu, (self.cut_action, self.copy_action, paste_action, self.delete_action, None, @@ -162,12 +165,14 @@ else: pline, pindex = self.current_prompt_pos self.setSelection(pline, pindex, line, index) - + + @Slot() def clear_line(self): """Clear current line (without clearing console prompt)""" if self.current_prompt_pos is not None: self.remove_text(self.current_prompt_pos, 'eof') - + + @Slot() def clear_terminal(self): """ Clear terminal window @@ -220,6 +225,7 @@ #------ Copy / Keyboard interrupt + @Slot() def copy(self): """Copy text to clipboard... or keyboard interrupt""" if self.has_selected_text(): @@ -229,27 +235,30 @@ def interrupt(self): """Keyboard interrupt""" - self.emit(SIGNAL("keyboard_interrupt()")) + self.sig_keyboard_interrupt.emit() + @Slot() def cut(self): """Cut text""" self.check_selection() if self.has_selected_text(): ConsoleBaseWidget.cut(self) + @Slot() def delete(self): """Remove selected text""" self.check_selection() if self.has_selected_text(): ConsoleBaseWidget.remove_selected_text(self) - + + @Slot() def save_historylog(self): """Save current history log (all text in console)""" title = _("Save history log") - self.emit(SIGNAL('redirect_stdio(bool)'), False) + self.redirect_stdio.emit(False) filename, _selfilter = getsavefilename(self, title, self.historylog_filename, "%s (*.log)" % _("History logs")) - self.emit(SIGNAL('redirect_stdio(bool)'), True) + self.redirect_stdio.emit(True) if filename: filename = osp.normpath(filename) try: @@ -271,7 +280,7 @@ self.execute_command(command) def execute_command(self, command): - self.emit(SIGNAL("execute(QString)"), command) + self.execute.emit(command) self.add_to_history(command) self.new_input_line = True @@ -280,7 +289,8 @@ self.set_cursor_position('eof') self.current_prompt_pos = self.get_position('cursor') self.new_input_line = False - + + @Slot() def paste(self): """Reimplemented slot to handle multiline paste action""" if self.new_input_line: @@ -657,28 +667,30 @@ # Example how to debug complex interclass call chains: # -# from spyderlib.utils.debug import log_methods_calls +# from spyder.utils.debug import log_methods_calls # log_methods_calls('log.log', ShellBaseWidget) class PythonShellWidget(TracebackLinksMixin, ShellBaseWidget, - InspectObjectMixin): + GetHelpMixin): """Python shell widget""" QT_CLASS = ShellBaseWidget - INITHISTORY = ['# -*- coding: utf-8 -*-', '# *** Spyder Python Console History Log ***',] SEPARATOR = '%s##---(%s)---' % (os.linesep*2, time.ctime()) + go_to_error = Signal(str) def __init__(self, parent, history_filename, profile=False): ShellBaseWidget.__init__(self, parent, history_filename, profile) TracebackLinksMixin.__init__(self) - InspectObjectMixin.__init__(self) + GetHelpMixin.__init__(self) # Local shortcuts self.shortcuts = self.create_shortcuts() - + def create_shortcuts(self): - inspectsc = create_shortcut(self.inspect_current_object, + fixed_shortcut(SHORTCUT_INLINE, self, lambda: self.enter_array_inline()) + fixed_shortcut(SHORTCUT_TABLE, self, lambda: self.enter_array_table()) + inspectsc = config_shortcut(self.inspect_current_object, context='Console', name='Inspect current object', parent=self) @@ -699,18 +711,18 @@ ShellBaseWidget.setup_context_menu(self) self.copy_without_prompts_action = create_action(self, _("Copy without prompts"), - icon=get_icon('copywop.png'), + icon=ima.icon('copywop'), triggered=self.copy_without_prompts) clear_line_action = create_action(self, _("Clear line"), QKeySequence(get_shortcut('console', 'Clear line')), - icon=get_icon('eraser.png'), + icon=ima.icon('editdelete'), tip=_("Clear line"), triggered=self.clear_line) clear_action = create_action(self, _("Clear shell"), QKeySequence(get_shortcut('console', 'Clear shell')), - icon=get_icon('clear.png'), + icon=ima.icon('editclear'), tip=_("Clear shell contents " "('cls' command)"), triggered=self.clear_terminal) @@ -722,7 +734,8 @@ state = self.has_selected_text() self.copy_without_prompts_action.setEnabled(state) ShellBaseWidget.contextMenuEvent(self, event) - + + @Slot() def copy_without_prompts(self): """Copy text to clipboard without prompts""" text = self.get_selected_text() @@ -880,6 +893,9 @@ text = to_text_string(self.get_current_line_to_cursor()) last_obj = self.get_last_obj() + if not text: + return + if text.startswith('import '): obj_list = self.get_module_completion(text) words = text.split(' ') @@ -968,6 +984,7 @@ COM = 'rem' if os.name == 'nt' else '#' INITHISTORY = ['%s *** Spyder Terminal History Log ***' % COM, COM,] SEPARATOR = '%s%s ---(%s)---' % (os.linesep*2, COM, time.ctime()) + go_to_error = Signal(str) def __init__(self, parent, history_filename, profile=False): ShellBaseWidget.__init__(self, parent, history_filename, profile) diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/widgets/sourcecode/base.py spyder-3.0.2+dfsg1/spyder/widgets/sourcecode/base.py --- spyder-2.3.8+dfsg1/spyder/widgets/sourcecode/base.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/widgets/sourcecode/base.py 2016-11-19 01:31:58.000000000 +0100 @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- # -# Copyright © 2009-2010 Pierre Raybaut +# Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) +# (see spyder/__init__.py for details) """QPlainTextEdit base class""" @@ -11,24 +11,41 @@ # pylint: disable=R0911 # pylint: disable=R0201 +# Standard library imports import os import re import sys -from spyderlib.qt.QtGui import (QTextCursor, QColor, QFont, QApplication, - QTextEdit, QTextCharFormat, QToolTip, - QListWidget, QPlainTextEdit, QPalette, - QMainWindow, QTextOption, QMouseEvent, - QTextFormat, QClipboard) -from spyderlib.qt.QtCore import SIGNAL, Qt, QEventLoop, QEvent, QPoint -from spyderlib.qt.compat import to_qvariant - +# Third party imports +from qtpy.compat import to_qvariant +from qtpy.QtCore import QEvent, QEventLoop, QPoint, Qt, Signal, Slot +from qtpy.QtGui import (QClipboard, QColor, QFont, QMouseEvent, QPalette, + QTextCharFormat, QTextFormat, QTextOption, QTextCursor) +from qtpy.QtWidgets import (QAbstractItemView, QApplication, QListWidget, + QListWidgetItem, QMainWindow, QPlainTextEdit, + QTextEdit, QToolTip) # Local imports -from spyderlib.widgets.sourcecode.terminal import ANSIEscapeCodeHandler -from spyderlib.widgets.mixins import BaseEditMixin -from spyderlib.widgets.calltip import CallTipWidget -from spyderlib.py3compat import to_text_string, str_lower, PY3 +from spyder.config.gui import get_font +from spyder.config.main import CONF +from spyder.py3compat import PY3, str_lower, to_text_string +from spyder.utils import icon_manager as ima +from spyder.widgets.calltip import CallTipWidget +from spyder.widgets.mixins import BaseEditMixin +from spyder.widgets.sourcecode.terminal import ANSIEscapeCodeHandler + + +def insert_text_to(cursor, text, fmt): + """Helper to print text, taking into account backspaces""" + while True: + index = text.find(chr(8)) # backspace + if index == -1: + break + cursor.insertText(text[:index], fmt) + if cursor.positionInBlock() > 0: + cursor.deletePreviousChar() + text = text[index+1:] + cursor.insertText(text, fmt) class CompletionWidget(QListWidget): @@ -41,23 +58,39 @@ self.case_sensitive = False self.enter_select = None self.hide() - self.connect(self, SIGNAL("itemActivated(QListWidgetItem*)"), - self.item_selected) + self.itemActivated.connect(self.item_selected) def setup_appearance(self, size, font): self.resize(*size) self.setFont(font) def show_list(self, completion_list, automatic=True): + types = [c[1] for c in completion_list] + completion_list = [c[0] for c in completion_list] if len(completion_list) == 1 and not automatic: self.textedit.insert_completion(completion_list[0]) return - + self.completion_list = completion_list self.clear() - self.addItems(completion_list) + + icons_map = {'instance': 'attribute', + 'statement': 'attribute', + 'method': 'method', + 'function': 'function', + 'class': 'class', + 'module': 'module'} + + self.type_list = types + if any(types): + for (c, t) in zip(completion_list, types): + icon = icons_map.get(t, 'no_match') + self.addItem(QListWidgetItem(ima.icon(icon), c)) + else: + self.addItems(completion_list) + self.setCurrentRow(0) - + QApplication.processEvents(QEventLoop.ExcludeUserInputEvents) self.show() self.setFocus() @@ -141,22 +174,27 @@ else: self.hide() QListWidget.keyPressEvent(self, event) - + def update_current(self): completion_text = to_text_string(self.textedit.completion_text) + if completion_text: for row, completion in enumerate(self.completion_list): if not self.case_sensitive: + print(completion_text) completion = completion.lower() completion_text = completion_text.lower() if completion.startswith(completion_text): self.setCurrentRow(row) + self.scrollTo(self.currentIndex(), + QAbstractItemView.PositionAtTop) break else: self.hide() else: self.hide() - + + def focusOutEvent(self, event): event.ignore() # Don't hide it on Mac when main window loses focus because @@ -179,6 +217,11 @@ """Text edit base widget""" BRACE_MATCHING_SCOPE = ('sof', 'eof') cell_separators = None + focus_in = Signal() + zoom_in = Signal() + zoom_out = Signal() + zoom_reset = Signal() + focus_changed = Signal() def __init__(self, parent=None): QPlainTextEdit.__init__(self, parent) @@ -187,9 +230,8 @@ self.extra_selections_dict = {} - self.connect(self, SIGNAL('textChanged()'), self.changed) - self.connect(self, SIGNAL('cursorPositionChanged()'), - self.cursor_position_changed) + self.textChanged.connect(self.changed) + self.cursorPositionChanged.connect(self.cursor_position_changed) self.indent_chars = " "*4 @@ -208,6 +250,7 @@ self.codecompletion_case = True self.codecompletion_enter = False self.completion_text = "" + self.setup_completion() self.calltip_widget = CallTipWidget(self, hide_timer_on=True) self.calltips = True @@ -225,13 +268,15 @@ self.bracepos = None self.matched_p_color = QColor(Qt.green) self.unmatched_p_color = QColor(Qt.red) - - def setup_completion(self, size=None, font=None): + + def setup_completion(self): + size = CONF.get('main', 'completion/size') + font = get_font() self.completion_widget.setup_appearance(size, font) - + def set_indent_chars(self, indent_chars): self.indent_chars = indent_chars - + def set_palette(self, background, foreground): """ Set text editor palette colors: @@ -277,8 +322,7 @@ def changed(self): """Emit changed signal""" - self.emit(SIGNAL('modificationChanged(bool)'), - self.document().isModified()) + self.modificationChanged.emit(self.document().isModified()) #------Highlight current line @@ -445,12 +489,14 @@ #------Reimplementing Qt methods + @Slot() def copy(self): """ Reimplement Qt method Copy text to clipboard with correct EOL chars """ - QApplication.clipboard().setText(self.get_selected_text()) + if self.get_selected_text(): + QApplication.clipboard().setText(self.get_selected_text()) def toPlainText(self): """ @@ -776,6 +822,7 @@ cursor = self.textCursor() cursor.beginEditBlock() start_pos, end_pos = self.__save_selection() + add_linesep = False if to_text_string(cursor.selectedText()): # Check if start_pos is at the start of a block cursor.setPosition(start_pos) @@ -795,22 +842,41 @@ start_pos = cursor.position() cursor.movePosition(QTextCursor.NextBlock) end_pos = cursor.position() + # check if on last line + if end_pos == start_pos: + cursor.movePosition(QTextCursor.End) + end_pos = cursor.position() + if start_pos == end_pos: + cursor.endEditBlock() + return + add_linesep = True cursor.setPosition(start_pos) cursor.setPosition(end_pos, QTextCursor.KeepAnchor) - + sel_text = to_text_string(cursor.selectedText()) + if add_linesep: + sel_text += os.linesep cursor.removeSelectedText() - + if after_current_line: text = to_text_string(cursor.block().text()) + if not text: + cursor.insertText(sel_text) + cursor.endEditBlock() + return start_pos += len(text)+1 end_pos += len(text)+1 cursor.movePosition(QTextCursor.NextBlock) + if cursor.position() < start_pos: + cursor.movePosition(QTextCursor.End) + sel_text = os.linesep + sel_text + end_pos -= 1 else: cursor.movePosition(QTextCursor.PreviousBlock) text = to_text_string(cursor.block().text()) start_pos -= len(text)+1 end_pos -= len(text)+1 + cursor.insertText(sel_text) cursor.endEditBlock() @@ -884,16 +950,21 @@ def show_completion_list(self, completions, completion_text="", automatic=True): """Display the possible completions""" - if completions is None or len(completions) == 0 or \ - completions == [completion_text]: + if not completions: + return + if not isinstance(completions[0], tuple): + completions = [(c, '') for c in completions] + if len(completions) == 1 and completions[0][0] == completion_text: return self.completion_text = completion_text # Sorting completion list (entries starting with underscore are # put at the end of the list): - underscore = set([comp for comp in completions + underscore = set([(comp, t) for (comp, t) in completions if comp.startswith('_')]) - completions = sorted(set(completions)-underscore, key=str_lower)+\ - sorted(underscore, key=str_lower) + + completions = sorted(set(completions) - underscore, + key=lambda x: str_lower(x[0])) + completions += sorted(underscore, key=lambda x: str_lower(x[0])) self.show_completion_widget(completions, automatic=automatic) def select_completion_list(self): @@ -1002,14 +1073,14 @@ def focusInEvent(self, event): """Reimplemented to handle focus""" - self.emit(SIGNAL("focus_changed()")) - self.emit(SIGNAL("focus_in()")) + self.focus_changed.emit() + self.focus_in.emit() self.highlight_current_cell() QPlainTextEdit.focusInEvent(self, event) def focusOutEvent(self, event): """Reimplemented to handle focus""" - self.emit(SIGNAL("focus_changed()")) + self.focus_changed.emit() QPlainTextEdit.focusOutEvent(self, event) def wheelEvent(self, event): @@ -1017,10 +1088,16 @@ # This feature is disabled on MacOS, see Issue 1510 if sys.platform != 'darwin': if event.modifiers() & Qt.ControlModifier: - if event.delta() < 0: - self.emit(SIGNAL("zoom_out()")) - elif event.delta() > 0: - self.emit(SIGNAL("zoom_in()")) + if hasattr(event, 'angleDelta'): + if event.angleDelta().y() < 0: + self.zoom_out.emit() + elif event.angleDelta().y() > 0: + self.zoom_in.emit() + elif hasattr(event, 'delta'): + if event.delta() < 0: + self.zoom_out.emit() + elif event.delta() > 0: + self.zoom_in.emit() return QPlainTextEdit.wheelEvent(self, event) self.highlight_current_cell() @@ -1122,11 +1199,15 @@ font.setItalic(self.italic) font.setUnderline(self.underline) self.format.setFont(font) - + + class ConsoleBaseWidget(TextEditBaseWidget): """Console base widget""" BRACE_MATCHING_SCOPE = ('sol', 'eol') COLOR_PATTERN = re.compile('\x01?\x1b\[(.*?)m\x02?') + traceback_available = Signal() + userListActivated = Signal(int, str) + completion_widget_activated = Signal(str) def __init__(self, parent=None): TextEditBaseWidget.__init__(self, parent) @@ -1141,10 +1222,8 @@ # Disable undo/redo (nonsense for a console widget...): self.setUndoRedoEnabled(False) - self.connect(self, SIGNAL('userListActivated(int, const QString)'), - lambda user_id, text: - self.emit(SIGNAL('completion_widget_activated(QString)'), - text)) + self.userListActivated.connect(lambda user_id, text: + self.completion_widget_activated.emit(text)) self.default_style = ConsoleFontStyle( foregroundcolor=0x000000, backgroundcolor=0xFFFFFF, @@ -1203,6 +1282,8 @@ #------Python shell def insert_text(self, text): """Reimplement TextEditBaseWidget method""" + # Eventually this maybe should wrap to insert_text_to if + # backspace-handling is required self.textCursor().insertText(text, self.default_style.format) def paste(self): @@ -1223,6 +1304,9 @@ """ cursor = self.textCursor() cursor.movePosition(QTextCursor.End) + if '\r' in text: # replace \r\n with \n + text = text.replace('\r\n', '\n') + text = text.replace('\r', '\n') while True: index = text.find(chr(12)) if index == -1: @@ -1243,21 +1327,24 @@ # Show error/warning messages in red cursor.insertText(text, self.error_style.format) if is_traceback: - self.emit(SIGNAL('traceback_available()')) + self.traceback_available.emit() elif prompt: # Show prompt in green - cursor.insertText(text, self.prompt_style.format) + insert_text_to(cursor, text, self.prompt_style.format) else: # Show other outputs in black last_end = 0 for match in self.COLOR_PATTERN.finditer(text): - cursor.insertText(text[last_end:match.start()], - self.default_style.format) + insert_text_to(cursor, text[last_end:match.start()], + self.default_style.format) last_end = match.end() - for code in [int(_c) for _c in match.group(1).split(';')]: - self.ansi_handler.set_code(code) + try: + for code in [int(_c) for _c in match.group(1).split(';')]: + self.ansi_handler.set_code(code) + except ValueError: + pass self.default_style.format = self.ansi_handler.get_format() - cursor.insertText(text[last_end:], self.default_style.format) + insert_text_to(cursor, text[last_end:], self.default_style.format) # # Slower alternative: # segments = self.COLOR_PATTERN.split(text) # cursor.insertText(segments.pop(0), self.default_style.format) @@ -1269,7 +1356,7 @@ # cursor.insertText(text, self.default_style.format) self.set_cursor_position('eof') self.setCurrentCharFormat(self.default_style.format) - + def set_pythonshell_font(self, font=None): """Python Shell only""" if font is None: diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/widgets/sourcecode/codeeditor.py spyder-3.0.2+dfsg1/spyder/widgets/sourcecode/codeeditor.py --- spyder-2.3.8+dfsg1/spyder/widgets/sourcecode/codeeditor.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/widgets/sourcecode/codeeditor.py 2016-11-19 01:31:58.000000000 +0100 @@ -1,84 +1,96 @@ # -*- coding: utf-8 -*- # -# Copyright © 2009-2010 Pierre Raybaut +# Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) +# (see spyder/__init__.py for details) """ Editor widget based on QtGui.QPlainTextEdit """ -#%% This line is for cell execution testing +# TODO: Try to separate this module from spyder to create a self +# consistent editor module (Qt source code and shell widgets library) + +# %% This line is for cell execution testing # pylint: disable=C0103 # pylint: disable=R0903 # pylint: disable=R0911 # pylint: disable=R0201 +# Standard library imports from __future__ import division - -import sys +from unicodedata import category +import os.path as osp import re import sre_constants -import os.path as osp +import sys import time -from spyderlib.qt import is_pyqt46 -from spyderlib.qt.QtGui import (QColor, QMenu, QApplication, QSplitter, QFont, - QTextEdit, QTextFormat, QPainter, QTextCursor, - QBrush, QTextDocument, QTextCharFormat, - QPixmap, QPrinter, QToolTip, QCursor, QLabel, - QInputDialog, QTextBlockUserData, QLineEdit, - QKeySequence, QWidget, QVBoxLayout, - QHBoxLayout, QDialog, QIntValidator, - QDialogButtonBox, QGridLayout, QPaintEvent, - QMessageBox, QTextOption) -from spyderlib.qt.QtCore import (Qt, SIGNAL, Signal, QTimer, QRect, QRegExp, - QSize, SLOT, Slot) -from spyderlib.qt.compat import to_qvariant - -#%% This line is for cell execution testing -# Local import -#TODO: Try to separate this module from spyderlib to create a self -# consistent editor module (Qt source code and shell widgets library) -from spyderlib.baseconfig import get_conf_path, _, DEBUG, get_image_path -from spyderlib.config import CONF -from spyderlib.guiconfig import get_font, create_shortcut -from spyderlib.utils.qthelpers import (add_actions, create_action, keybinding, - mimedata2url, get_icon) -from spyderlib.utils.dochelpers import getobj -from spyderlib.utils import encoding, sourcecode -from spyderlib.utils.sourcecode import ALL_LANGUAGES, CELL_LANGUAGES -from spyderlib.widgets.editortools import PythonCFM -from spyderlib.widgets.sourcecode.base import TextEditBaseWidget -from spyderlib.widgets.sourcecode import syntaxhighlighters as sh -from spyderlib.py3compat import to_text_string +# Third party imports +from qtpy import is_pyqt46 +from qtpy.compat import to_qvariant +from qtpy.QtCore import QRect, QRegExp, QSize, Qt, QTimer, Signal, Slot +from qtpy.QtGui import (QBrush, QColor, QCursor, QFont, QIntValidator, + QKeySequence, QPaintEvent, QPainter, + QTextBlockUserData, QTextCharFormat, QTextCursor, + QTextDocument, QTextFormat, QTextOption) +from qtpy.QtPrintSupport import QPrinter +from qtpy.QtWidgets import (QApplication, QDialog, QDialogButtonBox, + QGridLayout, QHBoxLayout, QInputDialog, QLabel, + QLineEdit, QMenu, QMessageBox, QSplitter, + QTextEdit, QToolTip, QVBoxLayout, QWidget) + +# %% This line is for cell execution testing + +# Local imports +from spyder.config.base import get_conf_path, _, DEBUG +from spyder.config.gui import (config_shortcut, fixed_shortcut, get_shortcut, + RUN_CELL_SHORTCUT, + RUN_CELL_AND_ADVANCE_SHORTCUT) +from spyder.config.main import CONF +from spyder.py3compat import to_text_string +from spyder.utils import icon_manager as ima +from spyder.utils import syntaxhighlighters as sh +from spyder.utils import encoding, sourcecode +from spyder.utils.dochelpers import getobj +from spyder.utils.qthelpers import add_actions, create_action, mimedata2url +from spyder.utils.sourcecode import ALL_LANGUAGES, CELL_LANGUAGES +from spyder.widgets.arraybuilder import SHORTCUT_INLINE, SHORTCUT_TABLE +from spyder.widgets.editortools import PythonCFM +from spyder.widgets.sourcecode.base import TextEditBaseWidget +from spyder.widgets.sourcecode.kill_ring import QtKillRing try: - import IPython.nbformat as nbformat - import IPython.nbformat.current # analysis:ignore - from IPython.nbconvert import PythonExporter as nbexporter + import nbformat as nbformat + from nbconvert import PythonExporter as nbexporter except: - nbformat = None # analysis:ignore + nbformat = None # analysis:ignore -#%% This line is for cell execution testing +# %% This line is for cell execution testing # For debugging purpose: LOG_FILENAME = get_conf_path('codeeditor.log') DEBUG_EDITOR = DEBUG >= 3 -#=============================================================================== +def is_letter_or_number(char): + """ Returns whether the specified unicode character is a letter or a number. + """ + cat = category(char) + return cat.startswith('L') or cat.startswith('N') + +# ============================================================================= # Go to line dialog box -#=============================================================================== +# ============================================================================= class GoToLineDialog(QDialog): def __init__(self, editor): QDialog.__init__(self, editor) - + # Destroying the C++ object right after closing the dialog box, # otherwise it may be garbage-collected in another QThread # (e.g. the editor's analysis thread in Spyder), thus leading to # a segmentation fault on UNIX or an application crash on Windows self.setAttribute(Qt.WA_DeleteOnClose) - + self.lineno = None self.editor = editor @@ -90,8 +102,7 @@ validator = QIntValidator(self.lineedit) validator.setRange(1, editor.get_line_count()) self.lineedit.setValidator(validator) - self.connect(self.lineedit, SIGNAL('textChanged(QString)'), - self.text_has_changed) + self.lineedit.textChanged.connect(self.text_has_changed) cl_label = QLabel(_("Current line:")) cl_label_v = QLabel("%d" % editor.get_cursor_line_number()) last_label = QLabel(_("Line count:")) @@ -107,15 +118,15 @@ bbox = QDialogButtonBox(QDialogButtonBox.Ok|QDialogButtonBox.Cancel, Qt.Vertical, self) - self.connect(bbox, SIGNAL("accepted()"), SLOT("accept()")) - self.connect(bbox, SIGNAL("rejected()"), SLOT("reject()")) + bbox.accepted.connect(self.accept) + bbox.rejected.connect(self.reject) btnlayout = QVBoxLayout() btnlayout.addWidget(bbox) btnlayout.addStretch(1) ok_button = bbox.button(QDialogButtonBox.Ok) ok_button.setEnabled(False) - self.connect(self.lineedit, SIGNAL("textChanged(QString)"), + self.lineedit.textChanged.connect( lambda text: ok_button.setEnabled(len(text) > 0)) layout = QHBoxLayout() @@ -124,7 +135,7 @@ self.setLayout(layout) self.lineedit.setFocus() - + def text_has_changed(self, text): """Line edit's text has changed""" text = to_text_string(text) @@ -184,13 +195,13 @@ WIDTH = 12 FLAGS_DX = 4 FLAGS_DY = 2 - + def __init__(self, editor): QWidget.__init__(self, editor) self.setAttribute(Qt.WA_OpaquePaintEvent) self.code_editor = editor - self.connect(editor.verticalScrollBar(), SIGNAL('valueChanged(int)'), - lambda value: self.repaint()) + editor.verticalScrollBar().valueChanged.connect( + lambda value: self.repaint()) def sizeHint(self): """Override Qt method""" @@ -205,7 +216,7 @@ vsb = self.code_editor.verticalScrollBar() value = self.position_to_value(event.pos().y()-1) vsb.setValue(value-.5*vsb.pageStep()) - + def get_scale_factor(self, slider=False): """Return scrollbar's scale factor: ratio between pixel span height and value span height""" @@ -214,13 +225,13 @@ position_height = vsb.height()-delta-1 value_height = vsb.maximum()-vsb.minimum()+vsb.pageStep() return float(position_height)/value_height - + def value_to_position(self, y, slider=False): """Convert value to position""" offset = 0 if slider else 1 vsb = self.code_editor.verticalScrollBar() return (y-vsb.minimum())*self.get_scale_factor(slider)+offset - + def position_to_value(self, y, slider=False): """Convert position to value""" offset = 0 if slider else 1 @@ -310,50 +321,46 @@ class CodeEditor(TextEditBaseWidget): """Source Code Editor Widget based exclusively on Qt""" - + LANGUAGES = {'Python': (sh.PythonSH, '#', PythonCFM), 'Cython': (sh.CythonSH, '#', PythonCFM), 'Fortran77': (sh.Fortran77SH, 'c', None), 'Fortran': (sh.FortranSH, '!', None), 'Idl': (sh.IdlSH, ';', None), - 'Matlab': (sh.MatlabSH, '%', None), 'Diff': (sh.DiffSH, '', None), 'GetText': (sh.GetTextSH, '#', None), 'Nsis': (sh.NsisSH, '#', None), 'Html': (sh.HtmlSH, '', None), - 'Css': (sh.CssSH, '', None), - 'Xml': (sh.XmlSH, '', None), - 'Js': (sh.JsSH, '//', None), - 'Json': (sh.JsonSH, '', None), - 'Julia': (sh.JuliaSH, '#', None), 'Yaml': (sh.YamlSH, '#', None), 'Cpp': (sh.CppSH, '//', None), 'OpenCL': (sh.OpenCLSH, '//', None), - 'Batch': (sh.BatchSH, 'rem ', None), - 'Ini': (sh.IniSH, '#', None), 'Enaml': (sh.EnamlSH, '#', PythonCFM), } - try: - import pygments # analysis:ignore - except ImportError: - # Removing all syntax highlighters requiring pygments to be installed - for key, (sh_class, comment_string, CFMatch) in list(LANGUAGES.items()): - if issubclass(sh_class, sh.PygmentsSH): - LANGUAGES.pop(key) - TAB_ALWAYS_INDENTS = ('py', 'pyw', 'python', 'c', 'cpp', 'cl', 'h') - # Custom signal to be emitted upon completion of the editor's paintEvent + # Custom signal to be emitted upon completion of the editor's paintEvent painted = Signal(QPaintEvent) - sig_new_file = Signal(str) - + # To have these attrs when early viewportEvent's are triggered edge_line = None linenumberarea = None - + + breakpoints_changed = Signal() + get_completions = Signal(bool) + go_to_definition = Signal(int) + sig_show_object_info = Signal(int) + run_selection = Signal() + run_cell_and_advance = Signal() + run_cell = Signal() + go_to_definition_regex = Signal(int) + sig_cursor_position_changed = Signal(int, int) + focus_changed = Signal() + sig_new_file = Signal(str) + def __init__(self, parent=None): TextEditBaseWidget.__init__(self, parent) + self.setFocusPolicy(Qt.StrongFocus) # We use these object names to set the right background @@ -364,19 +371,15 @@ plugin_name = repr(parent) if 'editor' in plugin_name.lower(): self.setObjectName('editor') - elif 'inspector' in plugin_name.lower(): - self.setObjectName('inspector') + elif 'help' in plugin_name.lower(): + self.setObjectName('help') elif 'historylog' in plugin_name.lower(): self.setObjectName('historylog') - - # Completion - completion_size = CONF.get('editor_appearance', 'completion/size') - completion_font = get_font('editor') - self.completion_widget.setup_appearance(completion_size, - completion_font) + elif 'configdialog' in plugin_name.lower(): + self.setObjectName('configdialog') # Caret (text cursor) - self.setCursorWidth( CONF.get('editor_appearance', 'cursor/width') ) + self.setCursorWidth( CONF.get('main', 'cursor/width') ) # 79-col edge line self.edge_line_enabled = True @@ -388,34 +391,31 @@ # Markers self.markers_margin = True self.markers_margin_width = 15 - self.error_pixmap = QPixmap(get_image_path('error.png'), 'png') - self.warning_pixmap = QPixmap(get_image_path('warning.png'), 'png') - self.todo_pixmap = QPixmap(get_image_path('todo.png'), 'png') - self.bp_pixmap = QPixmap(get_image_path('breakpoint_small.png'), 'png') - self.bpc_pixmap = QPixmap(get_image_path('breakpoint_cond_small.png'), - 'png') + self.error_pixmap = ima.icon('error').pixmap(QSize(14, 14)) + self.warning_pixmap = ima.icon('warning').pixmap(QSize(14, 14)) + self.todo_pixmap = ima.icon('todo').pixmap(QSize(14, 14)) + self.bp_pixmap = ima.icon('breakpoint_big').pixmap(QSize(14, 14)) + self.bpc_pixmap = ima.icon('breakpoint_cond_big').pixmap(QSize(14, 14)) # Line number area management self.linenumbers_margin = True self.linenumberarea_enabled = None self.linenumberarea = LineNumberArea(self) - self.connect(self, SIGNAL("blockCountChanged(int)"), - self.update_linenumberarea_width) - self.connect(self, SIGNAL("updateRequest(QRect,int)"), - self.update_linenumberarea) + self.blockCountChanged.connect(self.update_linenumberarea_width) + self.updateRequest.connect(self.update_linenumberarea) self.linenumberarea_pressed = -1 self.linenumberarea_released = -1 # Colors to be defined in _apply_highlighter_color_scheme() # Currentcell color and current line color are defined in base.py - self.occurence_color = None + self.occurrence_color = None self.ctrl_click_color = None self.sideareas_color = None self.matched_p_color = None self.unmatched_p_color = None self.normal_color = None self.comment_color = None - + self.linenumbers_color = QColor(Qt.darkGray) # --- Syntax highlight entrypoint --- @@ -428,7 +428,7 @@ # - providing data for Outliner # - self.highlighter is not responsible for # - background highlight for current line - # - background highlight for search / current line occurences + # - background highlight for search / current line occurrences self.highlighter_class = sh.TextSH self.highlighter = None @@ -452,9 +452,8 @@ self.document_id = id(self) - # Indicate occurences of the selected word - self.connect(self, SIGNAL('cursorPositionChanged()'), - self.__cursor_position_changed) + # Indicate occurrences of the selected word + self.cursorPositionChanged.connect(self.__cursor_position_changed) self.__find_first_pos = None self.__find_flags = None @@ -462,26 +461,25 @@ self.supported_cell_language = False self.classfunc_match = None self.comment_string = None + self._kill_ring = QtKillRing(self) # Block user data self.blockuserdata_list = [] - + # Update breakpoints if the number of lines in the file changes - self.connect(self, SIGNAL("blockCountChanged(int)"), - self.update_breakpoints) + self.blockCountChanged.connect(self.update_breakpoints) + + # Mark occurrences timer + self.occurrence_highlighting = None + self.occurrence_timer = QTimer(self) + self.occurrence_timer.setSingleShot(True) + self.occurrence_timer.setInterval(1500) + self.occurrence_timer.timeout.connect(self.__mark_occurrences) + self.occurrences = [] + self.occurrence_color = QColor(Qt.yellow).lighter(160) - # Mark occurences timer - self.occurence_highlighting = None - self.occurence_timer = QTimer(self) - self.occurence_timer.setSingleShot(True) - self.occurence_timer.setInterval(1500) - self.connect(self.occurence_timer, SIGNAL("timeout()"), - self.__mark_occurences) - self.occurences = [] - self.occurence_color = QColor(Qt.yellow).lighter(160) - # Mark found results - self.connect(self, SIGNAL('textChanged()'), self.__text_has_changed) + self.textChanged.connect(self.__text_has_changed) self.found_results = [] self.found_results_color = QColor(Qt.magenta).lighter(180) @@ -516,34 +514,122 @@ # Code editor self.__visible_blocks = [] # Visible blocks, update with repaint self.painted.connect(self._draw_editor_cell_divider) - - self.connect(self.verticalScrollBar(), SIGNAL('valueChanged(int)'), - lambda value: self.rehighlight_cells()) + + self.verticalScrollBar().valueChanged.connect( + lambda value: self.rehighlight_cells()) def create_shortcuts(self): - codecomp = create_shortcut(self.do_completion, context='Editor', - name='Code completion', parent=self) - duplicate_line = create_shortcut(self.duplicate_line, context='Editor', + codecomp = config_shortcut(self.do_completion, context='Editor', + name='Code Completion', parent=self) + duplicate_line = config_shortcut(self.duplicate_line, context='Editor', name='Duplicate line', parent=self) - copyline = create_shortcut(self.copy_line, context='Editor', + copyline = config_shortcut(self.copy_line, context='Editor', name='Copy line', parent=self) - deleteline = create_shortcut(self.delete_line, context='Editor', + deleteline = config_shortcut(self.delete_line, context='Editor', name='Delete line', parent=self) - movelineup = create_shortcut(self.move_line_up, context='Editor', + movelineup = config_shortcut(self.move_line_up, context='Editor', name='Move line up', parent=self) - movelinedown = create_shortcut(self.move_line_down, context='Editor', + movelinedown = config_shortcut(self.move_line_down, context='Editor', name='Move line down', parent=self) - gotodef = create_shortcut(self.do_go_to_definition, context='Editor', + gotodef = config_shortcut(self.do_go_to_definition, context='Editor', name='Go to definition', parent=self) - toggle_comment = create_shortcut(self.toggle_comment, context='Editor', + toggle_comment = config_shortcut(self.toggle_comment, context='Editor', name='Toggle comment', parent=self) - blockcomment = create_shortcut(self.blockcomment, context='Editor', + blockcomment = config_shortcut(self.blockcomment, context='Editor', name='Blockcomment', parent=self) - unblockcomment = create_shortcut(self.unblockcomment, context='Editor', + unblockcomment = config_shortcut(self.unblockcomment, context='Editor', name='Unblockcomment', parent=self) + transform_uppercase = config_shortcut(self.transform_to_uppercase, + context='Editor', + name='Transform to uppercase', + parent=self) + transform_lowercase = config_shortcut(self.transform_to_lowercase, + context='Editor', + name='Transform to lowercase', + parent=self) + + def cb_maker(attr): + """Make a callback for cursor move event type, (e.g. "Start") + """ + def cursor_move_event(): + cursor = self.textCursor() + move_type = getattr(QTextCursor, attr) + cursor.movePosition(move_type) + self.setTextCursor(cursor) + return cursor_move_event + + line_start = config_shortcut(cb_maker('StartOfLine'), context='Editor', + name='Start of line', parent=self) + line_end = config_shortcut(cb_maker('EndOfLine'), context='Editor', + name='End of line', parent=self) + + prev_line = config_shortcut(cb_maker('Up'), context='Editor', + name='Previous line', parent=self) + next_line = config_shortcut(cb_maker('Down'), context='Editor', + name='Next line', parent=self) + + prev_char = config_shortcut(cb_maker('Left'), context='Editor', + name='Previous char', parent=self) + next_char = config_shortcut(cb_maker('Right'), context='Editor', + name='Next char', parent=self) + + prev_word = config_shortcut(cb_maker('StartOfWord'), context='Editor', + name='Previous word', parent=self) + next_word = config_shortcut(cb_maker('EndOfWord'), context='Editor', + name='Next word', parent=self) + + kill_line_end = config_shortcut(self.kill_line_end, context='Editor', + name='Kill to line end', parent=self) + kill_line_start = config_shortcut(self.kill_line_start, + context='Editor', + name='Kill to line start', + parent=self) + yank = config_shortcut(self._kill_ring.yank, context='Editor', + name='Yank', parent=self) + kill_ring_rotate = config_shortcut(self._kill_ring.rotate, + context='Editor', + name='Rotate kill ring', + parent=self) + + kill_prev_word = config_shortcut(self.kill_prev_word, context='Editor', + name='Kill previous word', + parent=self) + kill_next_word = config_shortcut(self.kill_next_word, context='Editor', + name='Kill next word', parent=self) + + start_doc = config_shortcut(cb_maker('Start'), context='Editor', + name='Start of Document', parent=self) + + end_doc = config_shortcut(cb_maker('End'), context='Editor', + name='End of document', parent=self) + + undo = config_shortcut(self.undo, context='Editor', + name='undo', parent=self) + redo = config_shortcut(self.redo, context='Editor', + name='redo', parent=self) + cut = config_shortcut(self.cut, context='Editor', + name='cut', parent=self) + copy = config_shortcut(self.copy, context='Editor', + name='copy', parent=self) + paste = config_shortcut(self.paste, context='Editor', + name='paste', parent=self) + delete = config_shortcut(self.delete, context='Editor', + name='delete', parent=self) + select_all = config_shortcut(self.selectAll, context='Editor', + name='Select All', parent=self) + + # Fixed shortcuts + fixed_shortcut(SHORTCUT_INLINE, self, lambda: self.enter_array_inline()) + fixed_shortcut(SHORTCUT_TABLE, self, lambda: self.enter_array_table()) + return [codecomp, duplicate_line, copyline, deleteline, movelineup, movelinedown, gotodef, toggle_comment, blockcomment, - unblockcomment] + unblockcomment, transform_uppercase, transform_lowercase, + line_start, line_end, prev_line, next_line, + prev_char, next_char, prev_word, next_word, kill_line_end, + kill_line_start, yank, kill_ring_rotate, kill_prev_word, + kill_next_word, start_doc, end_doc, undo, redo, cut, copy, + paste, delete, select_all] def get_shortcut_data(self): """ @@ -557,8 +643,7 @@ def closeEvent(self, event): TextEditBaseWidget.closeEvent(self, event) if is_pyqt46: - self.emit(SIGNAL('destroyed()')) - + self.destroyed.emit() def get_document_id(self): return self.document_id @@ -578,15 +663,15 @@ def setup_editor(self, linenumbers=True, language=None, markers=False, font=None, color_scheme=None, wrap=False, tab_mode=True, intelligent_backspace=True, highlight_current_line=True, - highlight_current_cell=True, occurence_highlighting=True, + highlight_current_cell=True, occurrence_highlighting=True, scrollflagarea=True, edge_line=True, edge_line_column=79, codecompletion_auto=False, codecompletion_case=True, codecompletion_enter=False, show_blanks=False, calltips=None, go_to_definition=False, close_parentheses=True, close_quotes=False, add_colons=True, auto_unindent=True, indent_chars=" "*4, - tab_stop_width=40, cloned_from=None, - occurence_timeout=1500): + tab_stop_width=40, cloned_from=None, filename=None, + occurrence_timeout=1500): # Code completion and calltips self.set_codecompletion_auto(codecompletion_auto) @@ -617,7 +702,7 @@ self.setup_margins(linenumbers, markers) # Lexer - self.set_language(language) + self.set_language(language, filename) # Highlight current cell self.set_highlight_current_cell(highlight_current_cell) @@ -625,9 +710,9 @@ # Highlight current line self.set_highlight_current_line(highlight_current_line) - # Occurence highlighting - self.set_occurence_highlighting(occurence_highlighting) - self.set_occurence_timeout(occurence_timeout) + # Occurrence highlighting + self.set_occurrence_highlighting(occurrence_highlighting) + self.set_occurrence_timeout(occurrence_timeout) # Tab always indents (even when cursor is not at the begin of line) self.set_tab_mode(tab_mode) @@ -663,11 +748,11 @@ def set_close_parentheses_enabled(self, enable): """Enable/disable automatic parentheses insertion feature""" self.close_parentheses_enabled = enable - + def set_close_quotes_enabled(self, enable): """Enable/disable automatic quote insertion feature""" self.close_quotes_enabled = enable - + def set_add_colons_enabled(self, enable): """Enable/disable automatic colons insertion feature""" self.add_colons_enabled = enable @@ -676,15 +761,15 @@ """Enable/disable automatic unindent after else/elif/finally/except""" self.auto_unindent_enabled = enable - def set_occurence_highlighting(self, enable): - """Enable/disable occurence highlighting""" - self.occurence_highlighting = enable + def set_occurrence_highlighting(self, enable): + """Enable/disable occurrence highlighting""" + self.occurrence_highlighting = enable if not enable: - self.__clear_occurences() + self.__clear_occurrences() - def set_occurence_timeout(self, timeout): - """Set occurence highlighting timeout (ms)""" - self.occurence_timer.setInterval(timeout) + def set_occurrence_timeout(self, timeout): + """Set occurrence highlighting timeout (ms)""" + self.occurrence_timer.setInterval(timeout) def set_highlight_current_line(self, enable): """Enable/disable current line highlighting""" @@ -703,7 +788,7 @@ else: self.unhighlight_current_cell() - def set_language(self, language): + def set_language(self, language, filename=None): self.tab_indents = language in self.TAB_ALWAYS_INDENTS self.comment_string = '' sh_class = sh.TextSH @@ -721,6 +806,9 @@ else: self.classfunc_match = CFMatch() break + if filename is not None and not self.supported_language: + sh_class = sh.guess_pygments_highlighter(filename) + self.support_language = sh_class is not sh.TextSH self._set_highlighter(sh_class) def _set_highlighter(self, sh_class): @@ -736,20 +824,21 @@ self._apply_highlighter_color_scheme() def is_json(self): - return self.highlighter_class is sh.JsonSH + return (isinstance(self.highlighter, sh.PygmentsSH) and + self.highlighter._lexer.name == 'JSON') def is_python(self): return self.highlighter_class is sh.PythonSH def is_cython(self): return self.highlighter_class is sh.CythonSH - + def is_enaml(self): return self.highlighter_class is sh.EnamlSH def is_python_like(self): return self.is_python() or self.is_cython() or self.is_enaml() - + def intelligent_tab(self): """Provide intelligent behavoir for Tab key press""" leading_text = self.get_text('sol', 'cursor') @@ -762,7 +851,7 @@ elif leading_text.endswith('import ') or leading_text[-1] == '.': # blank import or dot completion self.do_completion() - elif (leading_text.split()[0] in ['from', 'import'] and + elif (leading_text.split()[0] in ['from', 'import'] and not ';' in leading_text): # import line with a single statement # (prevents lines like: `import pdb; pdb.set_trace()`) @@ -785,14 +874,14 @@ # blank line self.unindent() elif self.in_comment_or_string(): - self.unindent() + self.unindent() elif leading_text[-1] in '(,' or leading_text.endswith(', '): position = self.get_position('cursor') self.show_object_info(position) else: # if the line ends with any other character but comma self.unindent() - + def rehighlight(self): """ Rehighlight the whole document to rebuild outline explorer data @@ -803,12 +892,12 @@ if self.highlight_current_cell_enabled: self.highlight_current_cell() else: - self.unhighlight_current_cell() + self.unhighlight_current_cell() if self.highlight_current_line_enabled: self.highlight_current_line() else: self.unhighlight_current_line() - + def rehighlight_cells(self): """Rehighlight cells when moving the scrollbar""" if self.highlight_current_cell_enabled: @@ -855,9 +944,15 @@ offset = self.get_position('cursor') return sourcecode.get_primary_at(source_code, offset) - #------Find occurences + @Slot() + def delete(self): + """Remove selected text""" + if self.has_selected_text(): + self.remove_selected_text() + + #------Find occurrences def __find_first(self, text): - """Find first occurence: scan whole document""" + """Find first occurrence: scan whole document""" flags = QTextDocument.FindCaseSensitively|QTextDocument.FindWholeWords cursor = self.textCursor() # Scanning whole document @@ -868,7 +963,7 @@ return cursor def __find_next(self, text, cursor): - """Find next occurence""" + """Find next occurrence""" flags = QTextDocument.FindCaseSensitively|QTextDocument.FindWholeWords regexp = QRegExp(r"\b%s\b" % QRegExp.escape(text), Qt.CaseSensitive) cursor = self.document().find(regexp, cursor, flags) @@ -878,7 +973,7 @@ def __cursor_position_changed(self): """Cursor position has changed""" line, column = self.get_cursor_line_column() - self.emit(SIGNAL('cursorPositionChanged(int,int)'), line, column) + self.sig_cursor_position_changed.emit(line, column) if self.highlight_current_cell_enabled: self.highlight_current_cell() else: @@ -887,14 +982,14 @@ self.highlight_current_line() else: self.unhighlight_current_line() - if self.occurence_highlighting: - self.occurence_timer.stop() - self.occurence_timer.start() - - def __clear_occurences(self): - """Clear occurence markers""" - self.occurences = [] - self.clear_extra_selections('occurences') + if self.occurrence_highlighting: + self.occurrence_timer.stop() + self.occurrence_timer.start() + + def __clear_occurrences(self): + """Clear occurrence markers""" + self.occurrences = [] + self.clear_extra_selections('occurrences') self.scrollflagarea.update() def __highlight_selection(self, key, cursor, foreground_color=None, @@ -920,38 +1015,38 @@ if update: self.update_extra_selections() - def __mark_occurences(self): - """Marking occurences of the currently selected word""" - self.__clear_occurences() + def __mark_occurrences(self): + """Marking occurrences of the currently selected word""" + self.__clear_occurrences() if not self.supported_language: return - + text = self.get_current_word() if text is None: return if self.has_selected_text() and self.get_selected_text() != text: return - + if (self.is_python_like()) and \ (sourcecode.is_keyword(to_text_string(text)) or \ to_text_string(text) == 'self'): return - # Highlighting all occurences of word *text* + # Highlighting all occurrences of word *text* cursor = self.__find_first(text) - self.occurences = [] + self.occurrences = [] while cursor: - self.occurences.append(cursor.blockNumber()) - self.__highlight_selection('occurences', cursor, - background_color=self.occurence_color) + self.occurrences.append(cursor.blockNumber()) + self.__highlight_selection('occurrences', cursor, + background_color=self.occurrence_color) cursor = self.__find_next(text, cursor) self.update_extra_selections() - if len(self.occurences) > 1 and self.occurences[-1] == 0: + if len(self.occurrences) > 1 and self.occurrences[-1] == 0: # XXX: this is never happening with PySide but it's necessary - # for PyQt4... this must be related to a different behavior for + # for PyQt4... this must be related to a different behavior for # the QTextDocument.find function between those two libraries - self.occurences.pop(-1) + self.occurrences.pop(-1) self.scrollflagarea.update() #-----highlight found results (find/replace widget) @@ -981,13 +1076,13 @@ extra_selections.append(selection) self.set_extra_selections('find', extra_selections) self.update_extra_selections() - + def clear_found_results(self): """Clear found results highlighting""" self.found_results = [] self.clear_extra_selections('find') self.scrollflagarea.update() - + def __text_has_changed(self): """Text has changed, eventually clear found results highlighting""" if self.found_results: @@ -1028,7 +1123,7 @@ def update_linenumberarea_width(self, new_block_count=None): """ Update line number area width. - + new_block_count is needed to handle blockCountChanged(int) signal """ self.setViewportMargins(self.compute_linenumberarea_width(), 0, @@ -1226,7 +1321,7 @@ block.setUserData(data) self.linenumberarea.update() self.scrollflagarea.update() - self.emit(SIGNAL('breakpoints_changed()')) + self.breakpoints_changed.emit() def get_breakpoints(self): """Get breakpoints""" @@ -1256,22 +1351,22 @@ def update_breakpoints(self): """Update breakpoints""" - self.emit(SIGNAL('breakpoints_changed()')) + self.breakpoints_changed.emit() #-----Code introspection def do_completion(self, automatic=False): """Trigger completion""" if not self.is_completion_widget_visible(): - self.emit(SIGNAL('get_completions(bool)'), automatic) + self.get_completions.emit(automatic) def do_go_to_definition(self): """Trigger go-to-definition""" if not self.in_comment_or_string(): - self.emit(SIGNAL("go_to_definition(int)"), self.textCursor().position()) + self.go_to_definition.emit(self.textCursor().position()) def show_object_info(self, position): """Trigger a calltip""" - self.emit(SIGNAL('show_object_info(int)'), position) + self.sig_show_object_info.emit(position) #-----edge line def set_edge_line_enabled(self, state): @@ -1296,6 +1391,8 @@ else: option.setFlags(option.flags() & ~QTextOption.ShowTabsAndSpaces) self.document().setDefaultTextOption(option) + # Rehighlight to make the spaces less apparent. + self.rehighlight() #-----scrollflagarea def set_scrollflagarea_enabled(self, state): @@ -1315,12 +1412,12 @@ """Painting the scroll flag area""" make_flag = self.scrollflagarea.make_flag_qrect make_slider = self.scrollflagarea.make_slider_range - + # Filling the whole painting area painter = QPainter(self.scrollflagarea) painter.fillRect(event.rect(), self.sideareas_color) block = self.document().firstBlock() - + # Painting warnings and todos for line_number in range(1, self.document().blockCount()+1): data = block.userData() @@ -1344,14 +1441,14 @@ set_scrollflagarea_painter(painter, self.breakpoint_color) painter.drawRect(make_flag(position)) block = block.next() - - # Occurences - if self.occurences: - set_scrollflagarea_painter(painter, self.occurence_color) - for line_number in self.occurences: + + # Occurrences + if self.occurrences: + set_scrollflagarea_painter(painter, self.occurrence_color) + for line_number in self.occurrences: position = self.scrollflagarea.value_to_position(line_number) painter.drawRect(make_flag(position)) - + # Found results if self.found_results: set_scrollflagarea_painter(painter, self.found_results_color) @@ -1409,11 +1506,6 @@ return TextEditBaseWidget.viewportEvent(self, event) #-----Misc. - def delete(self): - """Remove selected text""" - # Used by global callbacks in Spyder -> delete_action - self.remove_selected_text() - def _apply_highlighter_color_scheme(self): """Apply color scheme from syntax highlighter to the editor""" hl = self.highlighter @@ -1422,7 +1514,7 @@ foreground=hl.get_foreground_color()) self.currentline_color = hl.get_currentline_color() self.currentcell_color = hl.get_currentcell_color() - self.occurence_color = hl.get_occurence_color() + self.occurrence_color = hl.get_occurrence_color() self.ctrl_click_color = hl.get_ctrlclick_color() self.sideareas_color = hl.get_sideareas_color() self.comment_color = hl.get_comment_color() @@ -1440,6 +1532,10 @@ else: self.highlighter.rehighlight() + def get_outlineexplorer_data(self): + """Get data provided by the Outline Explorer""" + return self.highlighter.get_outlineexplorer_data() + def set_font(self, font, color_scheme=None): """Set font""" # Note: why using this method to set color scheme instead of @@ -1479,7 +1575,7 @@ text, _enc = encoding.read(filename) if language is None: language = get_file_language(filename, text) - self.set_language(language) + self.set_language(language, filename) self.set_text(text) def append(self, text): @@ -1488,6 +1584,7 @@ cursor.movePosition(QTextCursor.End) cursor.insertText(text) + @Slot() def paste(self): """ Reimplement QPlainTextEdit's method to fix the following issue: @@ -1521,8 +1618,7 @@ def center_cursor_on_next_focus(self): """QPlainTextEdit's "centerCursor" requires the widget to be visible""" self.centerCursor() - self.disconnect(self, SIGNAL("focus_in()"), - self.center_cursor_on_next_focus) + self.focus_in.disconnect(self.center_cursor_on_next_focus) def go_to_line(self, line, word=''): """Go to line number *line* and eventually highlight it""" @@ -1531,8 +1627,7 @@ if self.isVisible(): self.centerCursor() else: - self.connect(self, SIGNAL("focus_in()"), - self.center_cursor_on_next_focus) + self.focus_in.connect(self.center_cursor_on_next_focus) self.horizontalScrollBar().setValue(0) if word and to_text_string(word) in to_text_string(block.text()): self.find(word, QTextDocument.FindCaseSensitively) @@ -1553,7 +1648,7 @@ del data self.setUpdatesEnabled(True) # When the new code analysis results are empty, it is necessary - # to update manually the scrollflag and linenumber areas (otherwise, + # to update manually the scrollflag and linenumber areas (otherwise, # the old flags will still be displayed): self.scrollflagarea.update() self.linenumberarea.update() @@ -1596,7 +1691,7 @@ regexp = QRegExp(r"\b%s\b" % QRegExp.escape(text), Qt.CaseSensitive) color = self.error_color if error else self.warning_color - # Highlighting all occurences (this is a compromise as pyflakes + # Highlighting all occurrences (this is a compromise as pyflakes # do not provide the column number -- see Issue 709 on Spyder's # GoogleCode project website) cursor = document.find(regexp, cursor, flags) @@ -1814,16 +1909,31 @@ Returns True if indent needed to be fixed """ - if not self.is_python_like(): - return cursor = self.textCursor() block_nb = cursor.blockNumber() + # find the line that contains our scope + diff = 0 + add_indent = False prevline = None for prevline in range(block_nb-1, -1, -1): cursor.movePosition(QTextCursor.PreviousBlock) prevtext = to_text_string(cursor.block().text()).rstrip() - if not prevtext.strip().startswith('#'): - break + if (self.is_python_like() and not prevtext.strip().startswith('#') \ + and prevtext) or prevtext: + if prevtext.strip().endswith(')'): + comment_or_string = True # prevent further parsing + elif prevtext.strip().endswith(':') and self.is_python_like(): + add_indent = True + comment_or_string = True + if prevtext.count(')') > prevtext.count('('): + diff = prevtext.count(')') - prevtext.count('(') + continue + elif diff: + diff += prevtext.count(')') - prevtext.count('(') + if not diff: + break + else: + break if not prevline: return False @@ -1831,16 +1941,18 @@ indent = self.get_block_indentation(block_nb) correct_indent = self.get_block_indentation(prevline) + if add_indent: + correct_indent += len(self.indent_chars) + if not comment_or_string: - if prevtext.endswith(':'): + if prevtext.endswith(':') and self.is_python_like(): # Indent correct_indent += len(self.indent_chars) - elif prevtext.endswith('continue') or prevtext.endswith('break') \ - or prevtext.endswith('pass'): + elif (prevtext.endswith('continue') or prevtext.endswith('break') \ + or prevtext.endswith('pass')) and self.is_python_like(): # Unindent correct_indent -= len(self.indent_chars) - elif prevtext.endswith(',') \ - and len(re.split(r'\(|\{|\[', prevtext)) > 1: + elif len(re.split(r'\(|\{|\[', prevtext)) > 1: rlmap = {")":"(", "]":"[", "}":"{"} for par in rlmap: i_right = prevtext.rfind(par) @@ -1853,8 +1965,13 @@ else: break else: - prevexpr = re.split(r'\(|\{|\[', prevtext)[-1] - correct_indent = len(prevtext)-len(prevexpr) + if prevtext.strip(): + if len(re.split(r'\(|\{|\[', prevtext)) > 1: + #correct indent only if there are still opening brackets + prevexpr = re.split(r'\(|\{|\[', prevtext)[-1] + correct_indent = len(prevtext)-len(prevexpr) + else: + correct_indent = len(prevtext) if (forward and indent >= correct_indent) or \ (not forward and indent <= correct_indent): @@ -1863,52 +1980,46 @@ if correct_indent >= 0: cursor = self.textCursor() - cursor.beginEditBlock() cursor.movePosition(QTextCursor.StartOfBlock) cursor.setPosition(cursor.position()+indent, QTextCursor.KeepAnchor) cursor.removeSelectedText() cursor.insertText(self.indent_chars[0]*correct_indent) - cursor.endEditBlock() return True + @Slot() def clear_all_output(self): - """removes all ouput in the ipynb format (Json only)""" - if self.is_json() and nbformat is not None: - try: - nb = nbformat.current.reads(self.toPlainText(), 'json') - except Exception as e: - QMessageBox.critical(self, _('Removal error'), - _("It was not possible to remove outputs from " - "this notebook. The error is:\n\n") + \ - to_text_string(e)) - return - if nb.worksheets: - for cell in nb.worksheets[0].cells: + """Removes all ouput in the ipynb format (Json only)""" + try: + nb = nbformat.reads(self.toPlainText(), as_version=4) + if nb.cells: + for cell in nb.cells: if 'outputs' in cell: cell['outputs'] = [] if 'prompt_number' in cell: cell['prompt_number'] = None # We do the following rather than using self.setPlainText - # to benefit from QTextEdit's undo/redo feature. + # to benefit from QTextEdit's undo/redo feature. self.selectAll() - self.insertPlainText(nbformat.current.writes(nb, 'json')) - else: + self.insertPlainText(nbformat.writes(nb)) + except Exception as e: + QMessageBox.critical(self, _('Removal error'), + _("It was not possible to remove outputs from " + "this notebook. The error is:\n\n") + \ + to_text_string(e)) return + @Slot() def convert_notebook(self): """Convert an IPython notebook to a Python script in editor""" try: - try: # >3.0 - nb = nbformat.reads(self.toPlainText(), as_version=4) - except AttributeError: - nb = nbformat.current.reads(self.toPlainText(), 'json') + nb = nbformat.reads(self.toPlainText(), as_version=4) + script = nbexporter().from_notebook_node(nb)[0] except Exception as e: - QMessageBox.critical(self, _('Conversion error'), + QMessageBox.critical(self, _('Conversion error'), _("It was not possible to convert this " "notebook. The error is:\n\n") + \ to_text_string(e)) return - script = nbexporter().from_notebook_node(nb)[0] self.sig_new_file.emit(script) def indent(self, force=False): @@ -1973,6 +2084,7 @@ else: self.remove_prefix(self.indent_chars) + @Slot() def toggle_comment(self): """Toggle comment on current line or selection""" cursor = self.textCursor() @@ -2002,18 +2114,48 @@ self.comment() def comment(self): - """Comment current line or selection""" + """Comment current line or selection.""" self.add_prefix(self.comment_string) def uncomment(self): - """Uncomment current line or selection""" + """Uncomment current line or selection.""" self.remove_prefix(self.comment_string) def __blockcomment_bar(self): return self.comment_string + '='*(79-len(self.comment_string)) + def transform_to_uppercase(self): + """Change to uppercase current line or selection.""" + cursor = self.textCursor() + prev_pos = cursor.position() + selected_text = to_text_string(cursor.selectedText()) + + if len(selected_text) == 0: + prev_pos = cursor.position() + cursor.select(QTextCursor.WordUnderCursor) + selected_text = to_text_string(cursor.selectedText()) + + s = selected_text.upper() + cursor.insertText(s) + self.set_cursor_position(prev_pos) + + def transform_to_lowercase(self): + """Change to lowercase current line or selection.""" + cursor = self.textCursor() + prev_pos = cursor.position() + selected_text = to_text_string(cursor.selectedText()) + + if len(selected_text) == 0: + prev_pos = cursor.position() + cursor.select(QTextCursor.WordUnderCursor) + selected_text = to_text_string(cursor.selectedText()) + + s = selected_text.lower() + cursor.insertText(s) + self.set_cursor_position(prev_pos) + def blockcomment(self): - """Block comment current line or selection""" + """Block comment current line or selection.""" comline = self.__blockcomment_bar() + self.get_line_separator() cursor = self.textCursor() if self.has_selected_text(): @@ -2084,7 +2226,83 @@ cursor3.select(QTextCursor.BlockUnderCursor) cursor3.removeSelectedText() cursor3.endEditBlock() - + + #------Kill ring handlers + # Taken from Jupyter's QtConsole + # Copyright (c) 2001-2015, IPython Development Team + # Copyright (c) 2015-, Jupyter Development Team + def kill_line_end(self): + """Kill the text on the current line from the cursor forward""" + cursor = self.textCursor() + cursor.clearSelection() + cursor.movePosition(QTextCursor.EndOfLine, QTextCursor.KeepAnchor) + if not cursor.hasSelection(): + # Line deletion + cursor.movePosition(QTextCursor.NextBlock, + QTextCursor.KeepAnchor) + self._kill_ring.kill_cursor(cursor) + self.setTextCursor(cursor) + + def kill_line_start(self): + """Kill the text on the current line from the cursor backward""" + cursor = self.textCursor() + cursor.clearSelection() + cursor.movePosition(QTextCursor.StartOfBlock, + QTextCursor.KeepAnchor) + self._kill_ring.kill_cursor(cursor) + self.setTextCursor(cursor) + + def _get_word_start_cursor(self, position): + """Find the start of the word to the left of the given position. If a + sequence of non-word characters precedes the first word, skip over + them. (This emulates the behavior of bash, emacs, etc.) + """ + document = self.document() + position -= 1 + while (position and not + is_letter_or_number(document.characterAt(position))): + position -= 1 + while position and is_letter_or_number(document.characterAt(position)): + position -= 1 + cursor = self.textCursor() + cursor.setPosition(position + 1) + return cursor + + def _get_word_end_cursor(self, position): + """Find the end of the word to the right of the given position. If a + sequence of non-word characters precedes the first word, skip over + them. (This emulates the behavior of bash, emacs, etc.) + """ + document = self.document() + cursor = self.textCursor() + position = cursor.position() + cursor.movePosition(QTextCursor.End) + end = cursor.position() + while (position < end and + not is_letter_or_number(document.characterAt(position))): + position += 1 + while (position < end and + is_letter_or_number(document.characterAt(position))): + position += 1 + cursor.setPosition(position) + return cursor + + def kill_prev_word(self): + """Kill the previous word""" + position = self.textCursor().position() + cursor = self._get_word_start_cursor(position) + cursor.setPosition(position, QTextCursor.KeepAnchor) + self._kill_ring.kill_cursor(cursor) + self.setTextCursor(cursor) + + def kill_next_word(self): + """Kill the next word""" + position = self.textCursor().position() + cursor = self._get_word_end_cursor(position) + cursor.setPosition(position, QTextCursor.KeepAnchor) + self._kill_ring.kill_cursor(cursor) + self.setTextCursor(cursor) + #------Autoinsertion of quotes/colons def __get_current_color(self): """Get the syntax highlighting color for the current cursor position""" @@ -2093,7 +2311,7 @@ pos = cursor.position() - block.position() # relative pos within block layout = block.layout() block_formats = layout.additionalFormats() - + if block_formats: # To easily grab current format for autoinsert_colons if cursor.atBlockEnd(): @@ -2107,7 +2325,7 @@ return color else: return None - + def in_comment_or_string(self): """Is the cursor inside or next to a comment or string?""" if self.highlighter: @@ -2152,6 +2370,22 @@ match = self.find_brace_match(line_pos+pos, char, forward=True) if (match is None) or (match > line_pos+len(text)): return True + if char in [')', ']', '}']: + match = self.find_brace_match(line_pos+pos, char, forward=False) + if (match is None) or (match < line_pos): + return True + return False + + def __has_colon_not_in_brackets(self, text): + """ + Return whether a string has a colon which is not between brackets. + This function returns True if the given string has a colon which is + not between a pair of (round, square or curly) brackets. It assumes + that the brackets in the string are balanced. + """ + for pos, char in enumerate(text): + if char == ':' and not self.__unmatched_braces_in_line(text[:pos]): + return True return False def autoinsert_colons(self): @@ -2167,17 +2401,19 @@ return False elif self.__unmatched_braces_in_line(line_text): return False + elif self.__has_colon_not_in_brackets(line_text): + return False else: return True - + def __unmatched_quotes_in_line(self, text): """Return whether a string has open quotes. This simply counts whether the number of quote characters of either type in the string is odd. - + Take from the IPython project (in IPython/core/completer.py in v0.13) Spyder team: Add some changes to deal with escaped quotes - + - Copyright (C) 2008-2011 IPython Development Team - Copyright (C) 2001-2007 Fernando Perez. - Copyright (C) 2001 Python Software Foundation, www.python.org @@ -2197,7 +2433,7 @@ def __next_char(self): cursor = self.textCursor() - cursor.movePosition(QTextCursor.NextCharacter, + cursor.movePosition(QTextCursor.NextCharacter, QTextCursor.KeepAnchor) next_char = to_text_string(cursor.selectedText()) return next_char @@ -2216,7 +2452,7 @@ def autoinsert_quotes(self, key): """Control how to automatically insert quotes in various situations""" char = {Qt.Key_QuoteDbl: '"', Qt.Key_Apostrophe: '\''}[key] - + line_text = self.get_text('sol', 'eol') line_to_cursor = self.get_text('sol', 'cursor') cursor = self.textCursor() @@ -2265,61 +2501,76 @@ #=============================================================================== def setup_context_menu(self): """Setup context menu""" - self.undo_action = create_action(self, _("Undo"), - shortcut=keybinding('Undo'), - icon=get_icon('undo.png'), triggered=self.undo) - self.redo_action = create_action(self, _("Redo"), - shortcut=keybinding('Redo'), - icon=get_icon('redo.png'), triggered=self.redo) - self.cut_action = create_action(self, _("Cut"), - shortcut=keybinding('Cut'), - icon=get_icon('editcut.png'), triggered=self.cut) - self.copy_action = create_action(self, _("Copy"), - shortcut=keybinding('Copy'), - icon=get_icon('editcopy.png'), triggered=self.copy) - paste_action = create_action(self, _("Paste"), - shortcut=keybinding('Paste'), - icon=get_icon('editpaste.png'), triggered=self.paste) - self.delete_action = create_action(self, _("Delete"), - shortcut=keybinding('Delete'), - icon=get_icon('editdelete.png'), - triggered=self.delete) - selectall_action = create_action(self, _("Select All"), - shortcut=keybinding('SelectAll'), - icon=get_icon('selectall.png'), - triggered=self.selectAll) - toggle_comment_action = create_action(self, - _("Comment")+"/"+_("Uncomment"), - icon=get_icon("comment.png"), - triggered=self.toggle_comment) - self.clear_all_output_action = create_action(self, - _("Clear all ouput"), icon='ipython_console.png', - triggered=self.clear_all_output) - self.ipynb_convert_action = create_action(self, - _("Convert to Python script"), - triggered=self.convert_notebook, - icon='python.png') - self.gotodef_action = create_action(self, _("Go to definition"), - triggered=self.go_to_definition_from_cursor) - self.run_selection_action = create_action(self, - _("Run &selection or current line"), - icon='run_selection.png', - triggered=lambda: self.emit(SIGNAL('run_selection()'))) - zoom_in_action = create_action(self, _("Zoom in"), - QKeySequence(QKeySequence.ZoomIn), icon='zoom_in.png', - triggered=lambda: self.emit(SIGNAL('zoom_in()'))) - zoom_out_action = create_action(self, _("Zoom out"), - QKeySequence(QKeySequence.ZoomOut), icon='zoom_out.png', - triggered=lambda: self.emit(SIGNAL('zoom_out()'))) - zoom_reset_action = create_action(self, _("Zoom reset"), - QKeySequence("Ctrl+0"), - triggered=lambda: self.emit(SIGNAL('zoom_reset()'))) + self.undo_action = create_action( + self, _("Undo"), icon=ima.icon('undo'), + shortcut=get_shortcut('editor', 'undo'), triggered=self.undo) + self.redo_action = create_action( + self, _("Redo"), icon=ima.icon('redo'), + shortcut=get_shortcut('editor', 'redo'), triggered=self.redo) + self.cut_action = create_action( + self, _("Cut"), icon=ima.icon('editcut'), + shortcut=get_shortcut('editor', 'cut'), triggered=self.cut) + self.copy_action = create_action( + self, _("Copy"), icon=ima.icon('editcopy'), + shortcut=get_shortcut('editor', 'copy'), triggered=self.copy) + self.paste_action = create_action( + self, _("Paste"), icon=ima.icon('editpaste'), + shortcut=get_shortcut('editor', 'paste'), triggered=self.paste) + selectall_action = create_action( + self, _("Select All"), icon=ima.icon('selectall'), + shortcut=get_shortcut('editor', 'select all'), + triggered=self.selectAll) + toggle_comment_action = create_action( + self, _("Comment")+"/"+_("Uncomment"), icon=ima.icon('comment'), + shortcut=get_shortcut('editor', 'toggle comment'), + triggered=self.toggle_comment) + self.clear_all_output_action = create_action( + self, _("Clear all ouput"), icon=ima.icon('ipython_console'), + triggered=self.clear_all_output) + self.ipynb_convert_action = create_action( + self, _("Convert to Python script"), icon=ima.icon('python'), + triggered=self.convert_notebook) + self.gotodef_action = create_action( + self, _("Go to definition"), + shortcut=get_shortcut('editor', 'go to definition'), + triggered=self.go_to_definition_from_cursor) + + # Run actions + self.run_cell_action = create_action( + self, _("Run cell"), icon=ima.icon('run_cell'), + shortcut=QKeySequence(RUN_CELL_SHORTCUT), + triggered=self.run_cell.emit) + self.run_cell_and_advance_action = create_action( + self, _("Run cell and advance"), icon=ima.icon('run_cell'), + shortcut=QKeySequence(RUN_CELL_AND_ADVANCE_SHORTCUT), + triggered=self.run_cell_and_advance.emit) + self.run_selection_action = create_action( + self, _("Run &selection or current line"), + icon=ima.icon('run_selection'), + shortcut=get_shortcut('editor', 'run selection'), + triggered=self.run_selection.emit) + + # Zoom actions + zoom_in_action = create_action( + self, _("Zoom in"), icon=ima.icon('zoom_in'), + shortcut=QKeySequence(QKeySequence.ZoomIn), + triggered=self.zoom_in.emit) + zoom_out_action = create_action( + self, _("Zoom out"), icon=ima.icon('zoom_out'), + shortcut=QKeySequence(QKeySequence.ZoomOut), + triggered=self.zoom_out.emit) + zoom_reset_action = create_action( + self, _("Zoom reset"), shortcut=QKeySequence("Ctrl+0"), + triggered=self.zoom_reset.emit) + + # Build menu self.menu = QMenu(self) - actions_1 = [self.undo_action, self.redo_action, None, self.cut_action, - self.copy_action, paste_action, self.delete_action, None] - actions_2 = [selectall_action, None, zoom_in_action, zoom_out_action, - zoom_reset_action, None, toggle_comment_action, None, - self.run_selection_action, self.gotodef_action] + actions_1 = [self.run_cell_action, self.run_cell_and_advance_action, + self.run_selection_action, self.gotodef_action, None, + self.undo_action, self.redo_action, None, self.cut_action, + self.copy_action, self.paste_action, selectall_action] + actions_2 = [None, zoom_in_action, zoom_out_action, zoom_reset_action, + None, toggle_comment_action] if nbformat is not None: nb_actions = [self.clear_all_output_action, self.ipynb_convert_action, None] @@ -2327,8 +2578,8 @@ add_actions(self.menu, actions) else: actions = actions_1 + actions_2 - add_actions(self.menu, actions) - + add_actions(self.menu, actions) + # Read-only context-menu self.readonly_menu = QMenu(self) add_actions(self.readonly_menu, @@ -2342,26 +2593,54 @@ shift = event.modifiers() & Qt.ShiftModifier text = to_text_string(event.text()) if text: - self.__clear_occurences() + self.__clear_occurrences() if QToolTip.isVisible(): self.hide_tooltip_if_necessary(key) + + # Handle the Qt Builtin key sequences + checks = [('SelectAll', 'Select All'), + ('Copy', 'Copy'), + ('Cut', 'Cut'), + ('Paste', 'Paste')] + + for qname, name in checks: + seq = getattr(QKeySequence, qname) + sc = get_shortcut('editor', name) + default = QKeySequence(seq).toString() + if event == seq and sc != default: + # if we have overridden it, call our action + for shortcut in self.shortcuts: + qsc, name, keystr = shortcut.data + if keystr == default: + qsc.activated.emit() + event.ignore() + return + + # otherwise, pass it on to parent + event.ignore() + return + if key in (Qt.Key_Enter, Qt.Key_Return): if not shift and not ctrl: if self.add_colons_enabled and self.is_python_like() and \ self.autoinsert_colons(): + self.textCursor().beginEditBlock() self.insert_text(':' + self.get_line_separator()) self.fix_indent() + self.textCursor().endEditBlock() elif self.is_completion_widget_visible() \ and self.codecompletion_enter: self.select_completion_list() else: cmt_or_str = self.in_comment_or_string() + self.textCursor().beginEditBlock() TextEditBaseWidget.keyPressEvent(self, event) self.fix_indent(comment_or_string=cmt_or_str) + self.textCursor().endEditBlock() elif shift: - self.emit(SIGNAL('run_cell_and_advance()')) + self.run_cell_and_advance.emit() elif ctrl: - self.emit(SIGNAL('run_cell()')) + self.run_cell.emit() elif key == Qt.Key_Insert and not shift and not ctrl: self.setOverwriteMode(not self.overwriteMode()) elif key == Qt.Key_Backspace and not shift and not ctrl: @@ -2403,25 +2682,15 @@ elif key == Qt.Key_Home: self.stdkey_home(shift, ctrl) elif key == Qt.Key_End: - # See Issue 495: on MacOS X, it is necessary to redefine this + # See Issue 495: on MacOS X, it is necessary to redefine this # basic action which should have been implemented natively self.stdkey_end(shift, ctrl) elif text == '(' and not self.has_selected_text(): self.hide_completion_widget() - position = self.get_position('cursor') - s_trailing_text = self.get_text('cursor', 'eol').strip() - if self.close_parentheses_enabled and \ - (len(s_trailing_text) == 0 or \ - s_trailing_text[0] in (',', ')', ']', '}')): - self.insert_text('()') - cursor = self.textCursor() - cursor.movePosition(QTextCursor.PreviousCharacter) - self.setTextCursor(cursor) + if self.close_parentheses_enabled: + self.handle_close_parentheses(text) else: self.insert_text(text) - if self.is_python_like() and self.get_text('sol', 'cursor') and \ - self.calltips: - self.emit(SIGNAL('show_object_info(int)'), position) elif text in ('[', '{') and not self.has_selected_text() \ and self.close_parentheses_enabled: s_trailing_text = self.get_text('cursor', 'eol').strip() @@ -2490,6 +2759,22 @@ if self.is_completion_widget_visible() and text: self.completion_text += text + def handle_close_parentheses(self, text): + if not self.close_parentheses_enabled: + return + position = self.get_position('cursor') + rest = self.get_text('cursor', 'eol').rstrip() + if not rest or rest[0] in (',', ')', ']', '}'): + self.insert_text('()') + cursor = self.textCursor() + cursor.movePosition(QTextCursor.PreviousCharacter) + self.setTextCursor(cursor) + else: + self.insert_text(text) + if self.is_python_like() and self.get_text('sol', 'cursor') and \ + self.calltips: + self.sig_show_object_info.emit(position) + def mouseMoveEvent(self, event): """Underline words when pressing """ if self.has_selected_text(): @@ -2526,7 +2811,8 @@ self.__cursor_changed = False self.clear_extra_selections('ctrl_click') TextEditBaseWidget.leaveEvent(self, event) - + + @Slot() def go_to_definition_from_cursor(self, cursor=None): """Go to definition from cursor instance (QTextCursor)""" if not self.go_to_definition_enabled: @@ -2541,7 +2827,7 @@ cursor.select(QTextCursor.WordUnderCursor) text = to_text_string(cursor.selectedText()) if not text is None: - self.emit(SIGNAL("go_to_definition(int)"), position) + self.go_to_definition.emit(position) def mousePressEvent(self, event): """Reimplement Qt method""" @@ -2558,21 +2844,23 @@ nonempty_selection = self.has_selected_text() self.copy_action.setEnabled(nonempty_selection) self.cut_action.setEnabled(nonempty_selection) - self.delete_action.setEnabled(nonempty_selection) - self.clear_all_output_action.setVisible(self.is_json()) - self.ipynb_convert_action.setVisible(self.is_json()) - self.run_selection_action.setEnabled(nonempty_selection) + self.clear_all_output_action.setVisible(self.is_json() and \ + nbformat is not None) + self.ipynb_convert_action.setVisible(self.is_json() and \ + nbformat is not None) + self.run_cell_action.setVisible(self.is_python()) + self.run_cell_and_advance_action.setVisible(self.is_python()) self.run_selection_action.setVisible(self.is_python()) self.gotodef_action.setVisible(self.go_to_definition_enabled \ - and self.is_python_like()) - + and self.is_python_like()) + # Code duplication go_to_definition_from_cursor and mouse_move_event cursor = self.textCursor() text = to_text_string(cursor.selectedText()) if len(text) == 0: cursor.select(QTextCursor.WordUnderCursor) text = to_text_string(cursor.selectedText()) - + self.undo_action.setEnabled( self.document().isUndoAvailable()) self.redo_action.setEnabled( self.document().isRedoAvailable()) menu = self.menu @@ -2638,7 +2926,7 @@ pen.setStyle(Qt.SolidLine) pen.setBrush(cell_line_color) painter.setPen(pen) - + for top, line_number, block in self.visible_blocks: if self.is_cell_separator(block): painter.drawLine(4, top, self.width(), top) @@ -2656,6 +2944,10 @@ """ return self.__visible_blocks + def is_editor(self): + return True + + #=============================================================================== # CodeEditor's Printer #=============================================================================== @@ -2693,17 +2985,17 @@ self.editor = CodeEditor(self) self.editor.setup_editor(linenumbers=True, markers=True, tab_mode=False, font=QFont("Courier New", 10), - show_blanks=True, color_scheme='Pydev') + show_blanks=True, color_scheme='Zenburn') self.addWidget(self.editor) - from spyderlib.widgets.editortools import OutlineExplorerWidget + from spyder.widgets.editortools import OutlineExplorerWidget self.classtree = OutlineExplorerWidget(self) self.addWidget(self.classtree) - self.connect(self.classtree, SIGNAL("edit_goto(QString,int,QString)"), - lambda _fn, line, word: self.editor.go_to_line(line, word)) + self.classtree.edit_goto.connect( + lambda _fn, line, word: self.editor.go_to_line(line, word)) self.setStretchFactor(0, 4) self.setStretchFactor(1, 1) - self.setWindowIcon(get_icon('spyder.svg')) - + self.setWindowIcon(ima.icon('spyder')) + def load(self, filename): self.editor.set_text_from_file(filename) self.setWindowTitle("%s - %s (%s)" % (_("Editor"), @@ -2711,32 +3003,28 @@ osp.dirname(filename))) self.classtree.set_current_editor(self.editor, filename, False, False) + def test(fname): - from spyderlib.utils.qthelpers import qapplication - app = qapplication() - app.setStyle('Plastique') + from spyder.utils.qthelpers import qapplication + app = qapplication(test_time=5) win = TestWidget(None) win.show() win.load(fname) - win.resize(1000, 800) + win.resize(900, 700) - from spyderlib.utils.codeanalysis import (check_with_pyflakes, - check_with_pep8) + from spyder.utils.codeanalysis import (check_with_pyflakes, + check_with_pep8) source_code = to_text_string(win.editor.toPlainText()) - res = check_with_pyflakes(source_code, fname)#+\ -# check_with_pep8(source_code, fname) - win.editor.process_code_analysis(res) + results = check_with_pyflakes(source_code, fname) + \ + check_with_pep8(source_code, fname) + win.editor.process_code_analysis(results) sys.exit(app.exec_()) + if __name__ == '__main__': if len(sys.argv) > 1: fname = sys.argv[1] else: fname = __file__ -# fname = r"d:\Python\scintilla\src\LexCPP.cxx" -# fname = r"C:\Python26\Lib\pdb.py" -# fname = r"C:\Python26\Lib\ssl.py" -# fname = r"D:\Python\testouille.py" -# fname = r"C:\Python26\Lib\pydoc.py" test(fname) diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/widgets/sourcecode/__init__.py spyder-3.0.2+dfsg1/spyder/widgets/sourcecode/__init__.py --- spyder-2.3.8+dfsg1/spyder/widgets/sourcecode/__init__.py 2015-08-24 21:09:46.000000000 +0200 +++ spyder-3.0.2+dfsg1/spyder/widgets/sourcecode/__init__.py 2016-10-25 02:05:23.000000000 +0200 @@ -1,12 +1,12 @@ # -*- coding: utf-8 -*- # -# Copyright © 2009-2011 Pierre Raybaut +# Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) +# (see spyder/__init__.py for details) """ -spyderlib.widgets.sourcecode -============================ +spyder.widgets.sourcecode +========================= Source code related widgets (code editor, console) based exclusively on Qt """ diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/widgets/sourcecode/kill_ring.py spyder-3.0.2+dfsg1/spyder/widgets/sourcecode/kill_ring.py --- spyder-2.3.8+dfsg1/spyder/widgets/sourcecode/kill_ring.py 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/widgets/sourcecode/kill_ring.py 2016-10-25 02:05:23.000000000 +0200 @@ -0,0 +1,132 @@ +""" +A generic Emacs-style kill ring, as well as a Qt-specific version. +Copyright (c) 2001-2015, IPython Development Team +Copyright (c) 2015-, Jupyter Development Team +All rights reserved. +""" + +# Third party imports +from qtpy.QtCore import QObject +from qtpy.QtGui import QTextCursor +from qtpy.QtWidgets import QTextEdit, QPlainTextEdit + + +# ---------------------------------------------------------------------------- +# Classes +# ---------------------------------------------------------------------------- +class KillRing(object): + """ A generic Emacs-style kill ring. + """ + + def __init__(self): + self.clear() + + def clear(self): + """ Clears the kill ring. + """ + self._index = -1 + self._ring = [] + + def kill(self, text): + """ Adds some killed text to the ring. + """ + self._ring.append(text) + + def yank(self): + """ Yank back the most recently killed text. + + Returns + ------- + A text string or None. + """ + self._index = len(self._ring) + return self.rotate() + + def rotate(self): + """ Rotate the kill ring, then yank back the new top. + + Returns + ------- + A text string or None. + """ + self._index -= 1 + if self._index >= 0: + return self._ring[self._index] + return None + + +class QtKillRing(QObject): + """ A kill ring attached to Q[Plain]TextEdit. + """ + + # ------------------------------------------------------------------------- + # QtKillRing interface + # ------------------------------------------------------------------------- + + def __init__(self, text_edit): + """ Create a kill ring attached to the specified Qt text edit. + """ + assert isinstance(text_edit, (QTextEdit, QPlainTextEdit)) + super(QtKillRing, self).__init__() + + self._ring = KillRing() + self._prev_yank = None + self._skip_cursor = False + self._text_edit = text_edit + + text_edit.cursorPositionChanged.connect(self._cursor_position_changed) + + def clear(self): + """ Clears the kill ring. + """ + self._ring.clear() + self._prev_yank = None + + def kill(self, text): + """ Adds some killed text to the ring. + """ + self._ring.kill(text) + + def kill_cursor(self, cursor): + """ Kills the text selected by the give cursor. + """ + text = cursor.selectedText() + if text: + cursor.removeSelectedText() + self.kill(text) + + def yank(self): + """ Yank back the most recently killed text. + """ + text = self._ring.yank() + if text: + self._skip_cursor = True + cursor = self._text_edit.textCursor() + cursor.insertText(text) + self._prev_yank = text + + def rotate(self): + """ Rotate the kill ring, then yank back the new top. + """ + if self._prev_yank: + text = self._ring.rotate() + if text: + self._skip_cursor = True + cursor = self._text_edit.textCursor() + cursor.movePosition(QTextCursor.Left, + QTextCursor.KeepAnchor, + n=len(self._prev_yank)) + cursor.insertText(text) + self._prev_yank = text + + # ------------------------------------------------------------------------- + # Protected interface + # ------------------------------------------------------------------------- + + # ----- Signal handlers --------------------------------------------------- + + def _cursor_position_changed(self): + if self._skip_cursor: + self._skip_cursor = False + else: + self._prev_yank = None diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/widgets/sourcecode/syntaxhighlighters.py spyder-3.0.2+dfsg1/spyder/widgets/sourcecode/syntaxhighlighters.py --- spyder-2.3.8+dfsg1/spyder/widgets/sourcecode/syntaxhighlighters.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/widgets/sourcecode/syntaxhighlighters.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,917 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright © 2009-2010 Pierre Raybaut -# Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) - -""" -Editor widget syntax highlighters based on QtGui.QSyntaxHighlighter -(Python syntax highlighting rules are inspired from idlelib) -""" - -from __future__ import print_function - -import re -import keyword - -from spyderlib.qt.QtGui import (QColor, QApplication, QFont, - QSyntaxHighlighter, QCursor, QTextCharFormat) -from spyderlib.qt.QtCore import Qt - -# Local imports -from spyderlib import dependencies -from spyderlib.baseconfig import _ -from spyderlib.config import CONF -from spyderlib.py3compat import builtins, is_text_string, to_text_string -from spyderlib.utils.sourcecode import CELL_LANGUAGES - - -PYGMENTS_REQVER = '>=1.6' -dependencies.add("pygments", _("Syntax highlighting for Matlab, Julia and other " - "file types"), - required_version=PYGMENTS_REQVER) - - -#============================================================================== -# Constants -#============================================================================== -COLOR_SCHEME_KEYS = ("background", "currentline", "currentcell", "occurence", - "ctrlclick", "sideareas", "matched_p", "unmatched_p", - "normal", "keyword", "builtin", "definition", - "comment", "string", "number", "instance") -COLOR_SCHEME_NAMES = CONF.get('color_schemes', 'names') - - -#============================================================================== -# Auxiliary functions -#============================================================================== -def get_color_scheme(name): - """Get a color scheme from config using its name""" - name = name.lower() - scheme = {} - for key in COLOR_SCHEME_KEYS: - try: - scheme[key] = CONF.get('color_schemes', name+'/'+key) - except: - scheme[key] = CONF.get('color_schemes', 'spyder/'+key) - return scheme - - -#============================================================================== -# Syntax highlighting color schemes -#============================================================================== -class BaseSH(QSyntaxHighlighter): - """Base Syntax Highlighter Class""" - # Syntax highlighting rules: - PROG = None - # Syntax highlighting states (from one text block to another): - NORMAL = 0 - def __init__(self, parent, font=None, color_scheme='Spyder'): - QSyntaxHighlighter.__init__(self, parent) - - self.outlineexplorer_data = {} - - self.font = font - self._check_color_scheme(color_scheme) - if is_text_string(color_scheme): - self.color_scheme = get_color_scheme(color_scheme) - else: - self.color_scheme = color_scheme - - self.background_color = None - self.currentline_color = None - self.currentcell_color = None - self.occurence_color = None - self.ctrlclick_color = None - self.sideareas_color = None - self.matched_p_color = None - self.unmatched_p_color = None - - self.formats = None - self.setup_formats(font) - - self.cell_separators = None - - def get_background_color(self): - return QColor(self.background_color) - - def get_foreground_color(self): - """Return foreground ('normal' text) color""" - return self.formats["normal"].foreground().color() - - def get_currentline_color(self): - return QColor(self.currentline_color) - - def get_currentcell_color(self): - return QColor(self.currentcell_color) - - def get_occurence_color(self): - return QColor(self.occurence_color) - - def get_ctrlclick_color(self): - return QColor(self.ctrlclick_color) - - def get_sideareas_color(self): - return QColor(self.sideareas_color) - - def get_matched_p_color(self): - return QColor(self.matched_p_color) - - def get_unmatched_p_color(self): - return QColor(self.unmatched_p_color) - - def get_comment_color(self): - """ Return color for the comments """ - return self.formats['comment'].foreground().color() - - def get_color_name(self, fmt): - """Return color name assigned to a given format""" - return self.formats[fmt].foreground().color().name() - - def setup_formats(self, font=None): - base_format = QTextCharFormat() - if font is not None: - self.font = font - if self.font is not None: - base_format.setFont(self.font) - self.formats = {} - colors = self.color_scheme.copy() - self.background_color = colors.pop("background") - self.currentline_color = colors.pop("currentline") - self.currentcell_color = colors.pop("currentcell") - self.occurence_color = colors.pop("occurence") - self.ctrlclick_color = colors.pop("ctrlclick") - self.sideareas_color = colors.pop("sideareas") - self.matched_p_color = colors.pop("matched_p") - self.unmatched_p_color = colors.pop("unmatched_p") - for name, (color, bold, italic) in list(colors.items()): - format = QTextCharFormat(base_format) - format.setForeground(QColor(color)) - format.setBackground(QColor(self.background_color)) - if bold: - format.setFontWeight(QFont.Bold) - format.setFontItalic(italic) - self.formats[name] = format - - def _check_color_scheme(self, color_scheme): - if is_text_string(color_scheme): - assert color_scheme in COLOR_SCHEME_NAMES - else: - assert all([key in color_scheme for key in COLOR_SCHEME_KEYS]) - - def set_color_scheme(self, color_scheme): - self._check_color_scheme(color_scheme) - if is_text_string(color_scheme): - self.color_scheme = get_color_scheme(color_scheme) - else: - self.color_scheme = color_scheme - self.setup_formats() - self.rehighlight() - - def highlightBlock(self, text): - raise NotImplementedError - - def get_outlineexplorer_data(self): - return self.outlineexplorer_data - - def rehighlight(self): - self.outlineexplorer_data = {} - QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) - QSyntaxHighlighter.rehighlight(self) - QApplication.restoreOverrideCursor() - - -class TextSH(BaseSH): - """Simple Text Syntax Highlighter Class (do nothing)""" - def highlightBlock(self, text): - pass - - -class GenericSH(BaseSH): - """Generic Syntax Highlighter""" - # Syntax highlighting rules: - PROG = None # to be redefined in child classes - def highlightBlock(self, text): - text = to_text_string(text) - self.setFormat(0, len(text), self.formats["normal"]) - - match = self.PROG.search(text) - index = 0 - while match: - for key, value in list(match.groupdict().items()): - if value: - start, end = match.span(key) - index += end-start - self.setFormat(start, end-start, self.formats[key]) - - match = self.PROG.search(text, match.end()) - - -#============================================================================== -# Python syntax highlighter -#============================================================================== -def any(name, alternates): - "Return a named group pattern matching list of alternates." - return "(?P<%s>" % name + "|".join(alternates) + ")" - -def make_python_patterns(additional_keywords=[], additional_builtins=[]): - "Strongly inspired from idlelib.ColorDelegator.make_pat" - kw = r"\b" + any("keyword", keyword.kwlist+additional_keywords) + r"\b" - builtinlist = [str(name) for name in dir(builtins) - if not name.startswith('_')]+additional_builtins - builtin = r"([^.'\"\\#]\b|^)" + any("builtin", builtinlist) + r"\b" - comment = any("comment", [r"#[^\n]*"]) - instance = any("instance", [r"\bself\b"]) - number = any("number", - [r"\b[+-]?[0-9]+[lLjJ]?\b", - r"\b[+-]?0[xX][0-9A-Fa-f]+[lL]?\b", - r"\b[+-]?0[oO][0-7]+[lL]?\b", - r"\b[+-]?0[bB][01]+[lL]?\b", - r"\b[+-]?[0-9]+(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?[jJ]?\b"]) - sqstring = r"(\b[rRuU])?'[^'\\\n]*(\\.[^'\\\n]*)*'?" - dqstring = r'(\b[rRuU])?"[^"\\\n]*(\\.[^"\\\n]*)*"?' - uf_sqstring = r"(\b[rRuU])?'[^'\\\n]*(\\.[^'\\\n]*)*(\\)$(?!')$" - uf_dqstring = r'(\b[rRuU])?"[^"\\\n]*(\\.[^"\\\n]*)*(\\)$(?!")$' - sq3string = r"(\b[rRuU])?'''[^'\\]*((\\.|'(?!''))[^'\\]*)*(''')?" - dq3string = r'(\b[rRuU])?"""[^"\\]*((\\.|"(?!""))[^"\\]*)*(""")?' - uf_sq3string = r"(\b[rRuU])?'''[^'\\]*((\\.|'(?!''))[^'\\]*)*(\\)?(?!''')$" - uf_dq3string = r'(\b[rRuU])?"""[^"\\]*((\\.|"(?!""))[^"\\]*)*(\\)?(?!""")$' - string = any("string", [sq3string, dq3string, sqstring, dqstring]) - ufstring1 = any("uf_sqstring", [uf_sqstring]) - ufstring2 = any("uf_dqstring", [uf_dqstring]) - ufstring3 = any("uf_sq3string", [uf_sq3string]) - ufstring4 = any("uf_dq3string", [uf_dq3string]) - return "|".join([instance, kw, builtin, comment, - ufstring1, ufstring2, ufstring3, ufstring4, string, - number, any("SYNC", [r"\n"])]) - -class OutlineExplorerData(object): - CLASS, FUNCTION, STATEMENT, COMMENT, CELL = list(range(5)) - def __init__(self): - self.text = None - self.fold_level = None - self.def_type = None - self.def_name = None - - def is_not_class_nor_function(self): - return self.def_type not in (self.CLASS, self.FUNCTION) - - def is_comment(self): - return self.def_type in (self.COMMENT, self.CELL) - - def get_class_name(self): - if self.def_type == self.CLASS: - return self.def_name - - def get_function_name(self): - if self.def_type == self.FUNCTION: - return self.def_name - -class PythonSH(BaseSH): - """Python Syntax Highlighter""" - # Syntax highlighting rules: - PROG = re.compile(make_python_patterns(), re.S) - IDPROG = re.compile(r"\s+(\w+)", re.S) - ASPROG = re.compile(r".*?\b(as)\b") - # Syntax highlighting states (from one text block to another): - (NORMAL, INSIDE_SQ3STRING, INSIDE_DQ3STRING, - INSIDE_SQSTRING, INSIDE_DQSTRING) = list(range(5)) - DEF_TYPES = {"def": OutlineExplorerData.FUNCTION, - "class": OutlineExplorerData.CLASS} - # Comments suitable for Outline Explorer - OECOMMENT = re.compile('^(# ?--[-]+|##[#]+ )[ -]*[^- ]+') - - def __init__(self, parent, font=None, color_scheme='Spyder'): - BaseSH.__init__(self, parent, font, color_scheme) - self.import_statements = {} - self.found_cell_separators = False - self.cell_separators = CELL_LANGUAGES['Python'] - - def highlightBlock(self, text): - text = to_text_string(text) - prev_state = self.previousBlockState() - if prev_state == self.INSIDE_DQ3STRING: - offset = -4 - text = r'""" '+text - elif prev_state == self.INSIDE_SQ3STRING: - offset = -4 - text = r"''' "+text - elif prev_state == self.INSIDE_DQSTRING: - offset = -2 - text = r'" '+text - elif prev_state == self.INSIDE_SQSTRING: - offset = -2 - text = r"' "+text - else: - offset = 0 - prev_state = self.NORMAL - - oedata = None - import_stmt = None - - self.setFormat(0, len(text), self.formats["normal"]) - - state = self.NORMAL - match = self.PROG.search(text) - while match: - for key, value in list(match.groupdict().items()): - if value: - start, end = match.span(key) - start = max([0, start+offset]) - end = max([0, end+offset]) - if key == "uf_sq3string": - self.setFormat(start, end-start, - self.formats["string"]) - state = self.INSIDE_SQ3STRING - elif key == "uf_dq3string": - self.setFormat(start, end-start, - self.formats["string"]) - state = self.INSIDE_DQ3STRING - elif key == "uf_sqstring": - self.setFormat(start, end-start, - self.formats["string"]) - state = self.INSIDE_SQSTRING - elif key == "uf_dqstring": - self.setFormat(start, end-start, - self.formats["string"]) - state = self.INSIDE_DQSTRING - else: - self.setFormat(start, end-start, self.formats[key]) - if key == "comment": - if text.lstrip().startswith(self.cell_separators): - self.found_cell_separators = True - oedata = OutlineExplorerData() - oedata.text = to_text_string(text).strip() - oedata.fold_level = start - oedata.def_type = OutlineExplorerData.CELL - oedata.def_name = text.strip() - elif self.OECOMMENT.match(text.lstrip()): - oedata = OutlineExplorerData() - oedata.text = to_text_string(text).strip() - oedata.fold_level = start - oedata.def_type = OutlineExplorerData.COMMENT - oedata.def_name = text.strip() - elif key == "keyword": - if value in ("def", "class"): - match1 = self.IDPROG.match(text, end) - if match1: - start1, end1 = match1.span(1) - self.setFormat(start1, end1-start1, - self.formats["definition"]) - oedata = OutlineExplorerData() - oedata.text = to_text_string(text) - oedata.fold_level = start - oedata.def_type = self.DEF_TYPES[ - to_text_string(value)] - oedata.def_name = text[start1:end1] - elif value in ("elif", "else", "except", "finally", - "for", "if", "try", "while", - "with"): - if text.lstrip().startswith(value): - oedata = OutlineExplorerData() - oedata.text = to_text_string(text).strip() - oedata.fold_level = start - oedata.def_type = \ - OutlineExplorerData.STATEMENT - oedata.def_name = text.strip() - elif value == "import": - import_stmt = text.strip() - # color all the "as" words on same line, except - # if in a comment; cheap approximation to the - # truth - if '#' in text: - endpos = text.index('#') - else: - endpos = len(text) - while True: - match1 = self.ASPROG.match(text, end, - endpos) - if not match1: - break - start, end = match1.span(1) - self.setFormat(start, end-start, - self.formats["keyword"]) - - match = self.PROG.search(text, match.end()) - - self.setCurrentBlockState(state) - - if oedata is not None: - block_nb = self.currentBlock().blockNumber() - self.outlineexplorer_data[block_nb] = oedata - self.outlineexplorer_data['found_cell_separators'] = self.found_cell_separators - if import_stmt is not None: - block_nb = self.currentBlock().blockNumber() - self.import_statements[block_nb] = import_stmt - - def get_import_statements(self): - return list(self.import_statements.values()) - - def rehighlight(self): - self.import_statements = {} - self.found_cell_separators = False - BaseSH.rehighlight(self) - - -#============================================================================== -# Cython syntax highlighter -#============================================================================== -C_TYPES = 'bool char double enum float int long mutable short signed struct unsigned void' - -class CythonSH(PythonSH): - """Cython Syntax Highlighter""" - ADDITIONAL_KEYWORDS = ["cdef", "ctypedef", "cpdef", "inline", "cimport", - "DEF"] - ADDITIONAL_BUILTINS = C_TYPES.split() - PROG = re.compile(make_python_patterns(ADDITIONAL_KEYWORDS, - ADDITIONAL_BUILTINS), re.S) - IDPROG = re.compile(r"\s+([\w\.]+)", re.S) - - -#============================================================================== -# Enaml syntax highlighter -#============================================================================== -class EnamlSH(PythonSH): - """Enaml Syntax Highlighter""" - ADDITIONAL_KEYWORDS = ["enamldef", "template", "attr", "event", "const", "alias"] - ADDITIONAL_BUILTINS = [] - PROG = re.compile(make_python_patterns(ADDITIONAL_KEYWORDS, - ADDITIONAL_BUILTINS), re.S) - IDPROG = re.compile(r"\s+([\w\.]+)", re.S) - - -#============================================================================== -# C/C++ syntax highlighter -#============================================================================== -C_KEYWORDS1 = 'and and_eq bitand bitor break case catch const const_cast continue default delete do dynamic_cast else explicit export extern for friend goto if inline namespace new not not_eq operator or or_eq private protected public register reinterpret_cast return sizeof static static_cast switch template throw try typedef typeid typename union using virtual while xor xor_eq' -C_KEYWORDS2 = 'a addindex addtogroup anchor arg attention author b brief bug c class code date def defgroup deprecated dontinclude e em endcode endhtmlonly ifdef endif endlatexonly endlink endverbatim enum example exception f$ file fn hideinitializer htmlinclude htmlonly if image include ingroup internal invariant interface latexonly li line link mainpage name namespace nosubgrouping note overload p page par param post pre ref relates remarks return retval sa section see showinitializer since skip skipline subsection test throw todo typedef union until var verbatim verbinclude version warning weakgroup' -C_KEYWORDS3 = 'asm auto class compl false true volatile wchar_t' - -def make_generic_c_patterns(keywords, builtins, - instance=None, define=None, comment=None): - "Strongly inspired from idlelib.ColorDelegator.make_pat" - kw = r"\b" + any("keyword", keywords.split()) + r"\b" - builtin = r"\b" + any("builtin", builtins.split()+C_TYPES.split()) + r"\b" - if comment is None: - comment = any("comment", [r"//[^\n]*", r"\/\*(.*?)\*\/"]) - comment_start = any("comment_start", [r"\/\*"]) - comment_end = any("comment_end", [r"\*\/"]) - if instance is None: - instance = any("instance", [r"\bthis\b"]) - number = any("number", - [r"\b[+-]?[0-9]+[lL]?\b", - r"\b[+-]?0[xX][0-9A-Fa-f]+[lL]?\b", - r"\b[+-]?[0-9]+(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?\b"]) - sqstring = r"(\b[rRuU])?'[^'\\\n]*(\\.[^'\\\n]*)*'?" - dqstring = r'(\b[rRuU])?"[^"\\\n]*(\\.[^"\\\n]*)*"?' - string = any("string", [sqstring, dqstring]) - if define is None: - define = any("define", [r"#[^\n]*"]) - return "|".join([instance, kw, comment, string, number, - comment_start, comment_end, builtin, - define, any("SYNC", [r"\n"])]) - -def make_cpp_patterns(): - return make_generic_c_patterns(C_KEYWORDS1+' '+C_KEYWORDS2, C_KEYWORDS3) - -class CppSH(BaseSH): - """C/C++ Syntax Highlighter""" - # Syntax highlighting rules: - PROG = re.compile(make_cpp_patterns(), re.S) - # Syntax highlighting states (from one text block to another): - NORMAL = 0 - INSIDE_COMMENT = 1 - def __init__(self, parent, font=None, color_scheme=None): - BaseSH.__init__(self, parent, font, color_scheme) - - def highlightBlock(self, text): - text = to_text_string(text) - inside_comment = self.previousBlockState() == self.INSIDE_COMMENT - self.setFormat(0, len(text), - self.formats["comment" if inside_comment else "normal"]) - - match = self.PROG.search(text) - index = 0 - while match: - for key, value in list(match.groupdict().items()): - if value: - start, end = match.span(key) - index += end-start - if key == "comment_start": - inside_comment = True - self.setFormat(start, len(text)-start, - self.formats["comment"]) - elif key == "comment_end": - inside_comment = False - self.setFormat(start, end-start, - self.formats["comment"]) - elif inside_comment: - self.setFormat(start, end-start, - self.formats["comment"]) - elif key == "define": - self.setFormat(start, end-start, - self.formats["number"]) - else: - self.setFormat(start, end-start, self.formats[key]) - - match = self.PROG.search(text, match.end()) - - last_state = self.INSIDE_COMMENT if inside_comment else self.NORMAL - self.setCurrentBlockState(last_state) - - -def make_opencl_patterns(): - # Keywords: - kwstr1 = 'cl_char cl_uchar cl_short cl_ushort cl_int cl_uint cl_long cl_ulong cl_half cl_float cl_double cl_platform_id cl_device_id cl_context cl_command_queue cl_mem cl_program cl_kernel cl_event cl_sampler cl_bool cl_bitfield cl_device_type cl_platform_info cl_device_info cl_device_address_info cl_device_fp_config cl_device_mem_cache_type cl_device_local_mem_type cl_device_exec_capabilities cl_command_queue_properties cl_context_properties cl_context_info cl_command_queue_info cl_channel_order cl_channel_type cl_mem_flags cl_mem_object_type cl_mem_info cl_image_info cl_addressing_mode cl_filter_mode cl_sampler_info cl_map_flags cl_program_info cl_program_build_info cl_build_status cl_kernel_info cl_kernel_work_group_info cl_event_info cl_command_type cl_profiling_info cl_image_format' - # Constants: - kwstr2 = 'CL_FALSE, CL_TRUE, CL_PLATFORM_PROFILE, CL_PLATFORM_VERSION, CL_PLATFORM_NAME, CL_PLATFORM_VENDOR, CL_PLATFORM_EXTENSIONS, CL_DEVICE_TYPE_DEFAULT , CL_DEVICE_TYPE_CPU, CL_DEVICE_TYPE_GPU, CL_DEVICE_TYPE_ACCELERATOR, CL_DEVICE_TYPE_ALL, CL_DEVICE_TYPE, CL_DEVICE_VENDOR_ID, CL_DEVICE_MAX_COMPUTE_UNITS, CL_DEVICE_MAX_WORK_ITEM_DIMENSIONS, CL_DEVICE_MAX_WORK_GROUP_SIZE, CL_DEVICE_MAX_WORK_ITEM_SIZES, CL_DEVICE_PREFERRED_VECTOR_WIDTH_CHAR, CL_DEVICE_PREFERRED_VECTOR_WIDTH_SHORT, CL_DEVICE_PREFERRED_VECTOR_WIDTH_INT, CL_DEVICE_PREFERRED_VECTOR_WIDTH_LONG, CL_DEVICE_PREFERRED_VECTOR_WIDTH_FLOAT, CL_DEVICE_PREFERRED_VECTOR_WIDTH_DOUBLE, CL_DEVICE_MAX_CLOCK_FREQUENCY, CL_DEVICE_ADDRESS_BITS, CL_DEVICE_MAX_READ_IMAGE_ARGS, CL_DEVICE_MAX_WRITE_IMAGE_ARGS, CL_DEVICE_MAX_MEM_ALLOC_SIZE, CL_DEVICE_IMAGE2D_MAX_WIDTH, CL_DEVICE_IMAGE2D_MAX_HEIGHT, CL_DEVICE_IMAGE3D_MAX_WIDTH, CL_DEVICE_IMAGE3D_MAX_HEIGHT, CL_DEVICE_IMAGE3D_MAX_DEPTH, CL_DEVICE_IMAGE_SUPPORT, CL_DEVICE_MAX_PARAMETER_SIZE, CL_DEVICE_MAX_SAMPLERS, CL_DEVICE_MEM_BASE_ADDR_ALIGN, CL_DEVICE_MIN_DATA_TYPE_ALIGN_SIZE, CL_DEVICE_SINGLE_FP_CONFIG, CL_DEVICE_GLOBAL_MEM_CACHE_TYPE, CL_DEVICE_GLOBAL_MEM_CACHELINE_SIZE, CL_DEVICE_GLOBAL_MEM_CACHE_SIZE, CL_DEVICE_GLOBAL_MEM_SIZE, CL_DEVICE_MAX_CONSTANT_BUFFER_SIZE, CL_DEVICE_MAX_CONSTANT_ARGS, CL_DEVICE_LOCAL_MEM_TYPE, CL_DEVICE_LOCAL_MEM_SIZE, CL_DEVICE_ERROR_CORRECTION_SUPPORT, CL_DEVICE_PROFILING_TIMER_RESOLUTION, CL_DEVICE_ENDIAN_LITTLE, CL_DEVICE_AVAILABLE, CL_DEVICE_COMPILER_AVAILABLE, CL_DEVICE_EXECUTION_CAPABILITIES, CL_DEVICE_QUEUE_PROPERTIES, CL_DEVICE_NAME, CL_DEVICE_VENDOR, CL_DRIVER_VERSION, CL_DEVICE_PROFILE, CL_DEVICE_VERSION, CL_DEVICE_EXTENSIONS, CL_DEVICE_PLATFORM, CL_FP_DENORM, CL_FP_INF_NAN, CL_FP_ROUND_TO_NEAREST, CL_FP_ROUND_TO_ZERO, CL_FP_ROUND_TO_INF, CL_FP_FMA, CL_NONE, CL_READ_ONLY_CACHE, CL_READ_WRITE_CACHE, CL_LOCAL, CL_GLOBAL, CL_EXEC_KERNEL, CL_EXEC_NATIVE_KERNEL, CL_QUEUE_OUT_OF_ORDER_EXEC_MODE_ENABLE, CL_QUEUE_PROFILING_ENABLE, CL_CONTEXT_REFERENCE_COUNT, CL_CONTEXT_DEVICES, CL_CONTEXT_PROPERTIES, CL_CONTEXT_PLATFORM, CL_QUEUE_CONTEXT, CL_QUEUE_DEVICE, CL_QUEUE_REFERENCE_COUNT, CL_QUEUE_PROPERTIES, CL_MEM_READ_WRITE, CL_MEM_WRITE_ONLY, CL_MEM_READ_ONLY, CL_MEM_USE_HOST_PTR, CL_MEM_ALLOC_HOST_PTR, CL_MEM_COPY_HOST_PTR, CL_R, CL_A, CL_RG, CL_RA, CL_RGB, CL_RGBA, CL_BGRA, CL_ARGB, CL_INTENSITY, CL_LUMINANCE, CL_SNORM_INT8, CL_SNORM_INT16, CL_UNORM_INT8, CL_UNORM_INT16, CL_UNORM_SHORT_565, CL_UNORM_SHORT_555, CL_UNORM_INT_101010, CL_SIGNED_INT8, CL_SIGNED_INT16, CL_SIGNED_INT32, CL_UNSIGNED_INT8, CL_UNSIGNED_INT16, CL_UNSIGNED_INT32, CL_HALF_FLOAT, CL_FLOAT, CL_MEM_OBJECT_BUFFER, CL_MEM_OBJECT_IMAGE2D, CL_MEM_OBJECT_IMAGE3D, CL_MEM_TYPE, CL_MEM_FLAGS, CL_MEM_SIZECL_MEM_HOST_PTR, CL_MEM_HOST_PTR, CL_MEM_MAP_COUNT, CL_MEM_REFERENCE_COUNT, CL_MEM_CONTEXT, CL_IMAGE_FORMAT, CL_IMAGE_ELEMENT_SIZE, CL_IMAGE_ROW_PITCH, CL_IMAGE_SLICE_PITCH, CL_IMAGE_WIDTH, CL_IMAGE_HEIGHT, CL_IMAGE_DEPTH, CL_ADDRESS_NONE, CL_ADDRESS_CLAMP_TO_EDGE, CL_ADDRESS_CLAMP, CL_ADDRESS_REPEAT, CL_FILTER_NEAREST, CL_FILTER_LINEAR, CL_SAMPLER_REFERENCE_COUNT, CL_SAMPLER_CONTEXT, CL_SAMPLER_NORMALIZED_COORDS, CL_SAMPLER_ADDRESSING_MODE, CL_SAMPLER_FILTER_MODE, CL_MAP_READ, CL_MAP_WRITE, CL_PROGRAM_REFERENCE_COUNT, CL_PROGRAM_CONTEXT, CL_PROGRAM_NUM_DEVICES, CL_PROGRAM_DEVICES, CL_PROGRAM_SOURCE, CL_PROGRAM_BINARY_SIZES, CL_PROGRAM_BINARIES, CL_PROGRAM_BUILD_STATUS, CL_PROGRAM_BUILD_OPTIONS, CL_PROGRAM_BUILD_LOG, CL_BUILD_SUCCESS, CL_BUILD_NONE, CL_BUILD_ERROR, CL_BUILD_IN_PROGRESS, CL_KERNEL_FUNCTION_NAME, CL_KERNEL_NUM_ARGS, CL_KERNEL_REFERENCE_COUNT, CL_KERNEL_CONTEXT, CL_KERNEL_PROGRAM, CL_KERNEL_WORK_GROUP_SIZE, CL_KERNEL_COMPILE_WORK_GROUP_SIZE, CL_KERNEL_LOCAL_MEM_SIZE, CL_EVENT_COMMAND_QUEUE, CL_EVENT_COMMAND_TYPE, CL_EVENT_REFERENCE_COUNT, CL_EVENT_COMMAND_EXECUTION_STATUS, CL_COMMAND_NDRANGE_KERNEL, CL_COMMAND_TASK, CL_COMMAND_NATIVE_KERNEL, CL_COMMAND_READ_BUFFER, CL_COMMAND_WRITE_BUFFER, CL_COMMAND_COPY_BUFFER, CL_COMMAND_READ_IMAGE, CL_COMMAND_WRITE_IMAGE, CL_COMMAND_COPY_IMAGE, CL_COMMAND_COPY_IMAGE_TO_BUFFER, CL_COMMAND_COPY_BUFFER_TO_IMAGE, CL_COMMAND_MAP_BUFFER, CL_COMMAND_MAP_IMAGE, CL_COMMAND_UNMAP_MEM_OBJECT, CL_COMMAND_MARKER, CL_COMMAND_ACQUIRE_GL_OBJECTS, CL_COMMAND_RELEASE_GL_OBJECTS, command execution status, CL_COMPLETE, CL_RUNNING, CL_SUBMITTED, CL_QUEUED, CL_PROFILING_COMMAND_QUEUED, CL_PROFILING_COMMAND_SUBMIT, CL_PROFILING_COMMAND_START, CL_PROFILING_COMMAND_END, CL_CHAR_BIT, CL_SCHAR_MAX, CL_SCHAR_MIN, CL_CHAR_MAX, CL_CHAR_MIN, CL_UCHAR_MAX, CL_SHRT_MAX, CL_SHRT_MIN, CL_USHRT_MAX, CL_INT_MAX, CL_INT_MIN, CL_UINT_MAX, CL_LONG_MAX, CL_LONG_MIN, CL_ULONG_MAX, CL_FLT_DIG, CL_FLT_MANT_DIG, CL_FLT_MAX_10_EXP, CL_FLT_MAX_EXP, CL_FLT_MIN_10_EXP, CL_FLT_MIN_EXP, CL_FLT_RADIX, CL_FLT_MAX, CL_FLT_MIN, CL_FLT_EPSILON, CL_DBL_DIG, CL_DBL_MANT_DIG, CL_DBL_MAX_10_EXP, CL_DBL_MAX_EXP, CL_DBL_MIN_10_EXP, CL_DBL_MIN_EXP, CL_DBL_RADIX, CL_DBL_MAX, CL_DBL_MIN, CL_DBL_EPSILON, CL_SUCCESS, CL_DEVICE_NOT_FOUND, CL_DEVICE_NOT_AVAILABLE, CL_COMPILER_NOT_AVAILABLE, CL_MEM_OBJECT_ALLOCATION_FAILURE, CL_OUT_OF_RESOURCES, CL_OUT_OF_HOST_MEMORY, CL_PROFILING_INFO_NOT_AVAILABLE, CL_MEM_COPY_OVERLAP, CL_IMAGE_FORMAT_MISMATCH, CL_IMAGE_FORMAT_NOT_SUPPORTED, CL_BUILD_PROGRAM_FAILURE, CL_MAP_FAILURE, CL_INVALID_VALUE, CL_INVALID_DEVICE_TYPE, CL_INVALID_PLATFORM, CL_INVALID_DEVICE, CL_INVALID_CONTEXT, CL_INVALID_QUEUE_PROPERTIES, CL_INVALID_COMMAND_QUEUE, CL_INVALID_HOST_PTR, CL_INVALID_MEM_OBJECT, CL_INVALID_IMAGE_FORMAT_DESCRIPTOR, CL_INVALID_IMAGE_SIZE, CL_INVALID_SAMPLER, CL_INVALID_BINARY, CL_INVALID_BUILD_OPTIONS, CL_INVALID_PROGRAM, CL_INVALID_PROGRAM_EXECUTABLE, CL_INVALID_KERNEL_NAME, CL_INVALID_KERNEL_DEFINITION, CL_INVALID_KERNEL, CL_INVALID_ARG_INDEX, CL_INVALID_ARG_VALUE, CL_INVALID_ARG_SIZE, CL_INVALID_KERNEL_ARGS, CL_INVALID_WORK_DIMENSION, CL_INVALID_WORK_GROUP_SIZE, CL_INVALID_WORK_ITEM_SIZE, CL_INVALID_GLOBAL_OFFSET, CL_INVALID_EVENT_WAIT_LIST, CL_INVALID_EVENT, CL_INVALID_OPERATION, CL_INVALID_GL_OBJECT, CL_INVALID_BUFFER_SIZE, CL_INVALID_MIP_LEVEL, CL_INVALID_GLOBAL_WORK_SIZE' - # Functions: - builtins = 'clGetPlatformIDs, clGetPlatformInfo, clGetDeviceIDs, clGetDeviceInfo, clCreateContext, clCreateContextFromType, clReleaseContext, clGetContextInfo, clCreateCommandQueue, clRetainCommandQueue, clReleaseCommandQueue, clGetCommandQueueInfo, clSetCommandQueueProperty, clCreateBuffer, clCreateImage2D, clCreateImage3D, clRetainMemObject, clReleaseMemObject, clGetSupportedImageFormats, clGetMemObjectInfo, clGetImageInfo, clCreateSampler, clRetainSampler, clReleaseSampler, clGetSamplerInfo, clCreateProgramWithSource, clCreateProgramWithBinary, clRetainProgram, clReleaseProgram, clBuildProgram, clUnloadCompiler, clGetProgramInfo, clGetProgramBuildInfo, clCreateKernel, clCreateKernelsInProgram, clRetainKernel, clReleaseKernel, clSetKernelArg, clGetKernelInfo, clGetKernelWorkGroupInfo, clWaitForEvents, clGetEventInfo, clRetainEvent, clReleaseEvent, clGetEventProfilingInfo, clFlush, clFinish, clEnqueueReadBuffer, clEnqueueWriteBuffer, clEnqueueCopyBuffer, clEnqueueReadImage, clEnqueueWriteImage, clEnqueueCopyImage, clEnqueueCopyImageToBuffer, clEnqueueCopyBufferToImage, clEnqueueMapBuffer, clEnqueueMapImage, clEnqueueUnmapMemObject, clEnqueueNDRangeKernel, clEnqueueTask, clEnqueueNativeKernel, clEnqueueMarker, clEnqueueWaitForEvents, clEnqueueBarrier' - # Qualifiers: - qualifiers = '__global __local __constant __private __kernel' - keyword_list = C_KEYWORDS1+' '+C_KEYWORDS2+' '+kwstr1+' '+kwstr2 - builtin_list = C_KEYWORDS3+' '+builtins+' '+qualifiers - return make_generic_c_patterns(keyword_list, builtin_list) - -class OpenCLSH(CppSH): - """OpenCL Syntax Highlighter""" - PROG = re.compile(make_opencl_patterns(), re.S) - - -#============================================================================== -# Fortran Syntax Highlighter -#============================================================================== - -def make_fortran_patterns(): - "Strongly inspired from idlelib.ColorDelegator.make_pat" - kwstr = 'access action advance allocatable allocate apostrophe assign assignment associate asynchronous backspace bind blank blockdata call case character class close common complex contains continue cycle data deallocate decimal delim default dimension direct do dowhile double doubleprecision else elseif elsewhere encoding end endassociate endblockdata enddo endfile endforall endfunction endif endinterface endmodule endprogram endselect endsubroutine endtype endwhere entry eor equivalence err errmsg exist exit external file flush fmt forall form format formatted function go goto id if implicit in include inout integer inquire intent interface intrinsic iomsg iolength iostat kind len logical module name named namelist nextrec nml none nullify number only open opened operator optional out pad parameter pass pause pending pointer pos position precision print private program protected public quote read readwrite real rec recl recursive result return rewind save select selectcase selecttype sequential sign size stat status stop stream subroutine target then to type unformatted unit use value volatile wait where while write' - bistr1 = 'abs achar acos acosd adjustl adjustr aimag aimax0 aimin0 aint ajmax0 ajmin0 akmax0 akmin0 all allocated alog alog10 amax0 amax1 amin0 amin1 amod anint any asin asind associated atan atan2 atan2d atand bitest bitl bitlr bitrl bjtest bit_size bktest break btest cabs ccos cdabs cdcos cdexp cdlog cdsin cdsqrt ceiling cexp char clog cmplx conjg cos cosd cosh count cpu_time cshift csin csqrt dabs dacos dacosd dasin dasind datan datan2 datan2d datand date date_and_time dble dcmplx dconjg dcos dcosd dcosh dcotan ddim dexp dfloat dflotk dfloti dflotj digits dim dimag dint dlog dlog10 dmax1 dmin1 dmod dnint dot_product dprod dreal dsign dsin dsind dsinh dsqrt dtan dtand dtanh eoshift epsilon errsns exp exponent float floati floatj floatk floor fraction free huge iabs iachar iand ibclr ibits ibset ichar idate idim idint idnint ieor ifix iiabs iiand iibclr iibits iibset iidim iidint iidnnt iieor iifix iint iior iiqint iiqnnt iishft iishftc iisign ilen imax0 imax1 imin0 imin1 imod index inint inot int int1 int2 int4 int8 iqint iqnint ior ishft ishftc isign isnan izext jiand jibclr jibits jibset jidim jidint jidnnt jieor jifix jint jior jiqint jiqnnt jishft jishftc jisign jmax0 jmax1 jmin0 jmin1 jmod jnint jnot jzext kiabs kiand kibclr kibits kibset kidim kidint kidnnt kieor kifix kind kint kior kishft kishftc kisign kmax0 kmax1 kmin0 kmin1 kmod knint knot kzext lbound leadz len len_trim lenlge lge lgt lle llt log log10 logical lshift malloc matmul max max0 max1 maxexponent maxloc maxval merge min min0 min1 minexponent minloc minval mod modulo mvbits nearest nint not nworkers number_of_processors pack popcnt poppar precision present product radix random random_number random_seed range real repeat reshape rrspacing rshift scale scan secnds selected_int_kind selected_real_kind set_exponent shape sign sin sind sinh size sizeof sngl snglq spacing spread sqrt sum system_clock tan tand tanh tiny transfer transpose trim ubound unpack verify' - bistr2 = 'cdabs cdcos cdexp cdlog cdsin cdsqrt cotan cotand dcmplx dconjg dcotan dcotand decode dimag dll_export dll_import doublecomplex dreal dvchk encode find flen flush getarg getcharqq getcl getdat getenv gettim hfix ibchng identifier imag int1 int2 int4 intc intrup invalop iostat_msg isha ishc ishl jfix lacfar locking locnear map nargs nbreak ndperr ndpexc offset ovefl peekcharqq precfill prompt qabs qacos qacosd qasin qasind qatan qatand qatan2 qcmplx qconjg qcos qcosd qcosh qdim qexp qext qextd qfloat qimag qlog qlog10 qmax1 qmin1 qmod qreal qsign qsin qsind qsinh qsqrt qtan qtand qtanh ran rand randu rewrite segment setdat settim system timer undfl unlock union val virtual volatile zabs zcos zexp zlog zsin zsqrt' - kw = r"\b" + any("keyword", kwstr.split()) + r"\b" - builtin = r"\b" + any("builtin", bistr1.split()+bistr2.split()) + r"\b" - comment = any("comment", [r"\![^\n]*"]) - number = any("number", - [r"\b[+-]?[0-9]+[lL]?\b", - r"\b[+-]?0[xX][0-9A-Fa-f]+[lL]?\b", - r"\b[+-]?[0-9]+(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?\b"]) - sqstring = r"(\b[rRuU])?'[^'\\\n]*(\\.[^'\\\n]*)*'?" - dqstring = r'(\b[rRuU])?"[^"\\\n]*(\\.[^"\\\n]*)*"?' - string = any("string", [sqstring, dqstring]) - return "|".join([kw, comment, string, number, builtin, - any("SYNC", [r"\n"])]) - -class FortranSH(BaseSH): - """Fortran Syntax Highlighter""" - # Syntax highlighting rules: - PROG = re.compile(make_fortran_patterns(), re.S|re.I) - IDPROG = re.compile(r"\s+(\w+)", re.S) - # Syntax highlighting states (from one text block to another): - NORMAL = 0 - def __init__(self, parent, font=None, color_scheme=None): - BaseSH.__init__(self, parent, font, color_scheme) - - def highlightBlock(self, text): - text = to_text_string(text) - self.setFormat(0, len(text), self.formats["normal"]) - - match = self.PROG.search(text) - index = 0 - while match: - for key, value in list(match.groupdict().items()): - if value: - start, end = match.span(key) - index += end-start - self.setFormat(start, end-start, self.formats[key]) - if value.lower() in ("subroutine", "module", "function"): - match1 = self.IDPROG.match(text, end) - if match1: - start1, end1 = match1.span(1) - self.setFormat(start1, end1-start1, - self.formats["definition"]) - - match = self.PROG.search(text, match.end()) - -class Fortran77SH(FortranSH): - """Fortran 77 Syntax Highlighter""" - def highlightBlock(self, text): - text = to_text_string(text) - if text.startswith(("c", "C")): - self.setFormat(0, len(text), self.formats["comment"]) - else: - FortranSH.highlightBlock(self, text) - self.setFormat(0, 5, self.formats["comment"]) - self.setFormat(73, max([73, len(text)]), - self.formats["comment"]) - - -#============================================================================== -# IDL highlighter -# -# Contribution from Stuart Mumford (Littlemumford) - 02/02/2012 -# See Issue #850 -#============================================================================== -def make_idl_patterns(): - "Strongly inspired from idlelib.ColorDelegator.make_pat" - kwstr = 'begin of pro function endfor endif endwhile endrep endcase endswitch end if then else for do while repeat until break case switch common continue exit return goto help message print read retall stop' - bistr1 = 'a_correlate abs acos adapt_hist_equal alog alog10 amoeba arg_present arra_equal array_indices ascii_template asin assoc atan beseli beselj besel k besely beta bilinear bin_date binary_template dinfgen dinomial blk_con broyden bytarr byte bytscl c_correlate call_external call_function ceil chebyshev check_math chisqr_cvf chisqr_pdf choldc cholsol cindgen clust_wts cluster color_quan colormap_applicable comfit complex complexarr complexround compute_mesh_normals cond congrid conj convert_coord convol coord2to3 correlate cos cosh cramer create_struct crossp crvlength ct_luminance cti_test curvefit cv_coord cvttobm cw_animate cw_arcball cw_bgroup cw_clr_index cw_colorsel cw_defroi cw_field cw_filesel cw_form cw_fslider cw_light_editor cw_orient cw_palette_editor cw_pdmenu cw_rgbslider cw_tmpl cw_zoom dblarr dcindgen dcomplexarr defroi deriv derivsig determ diag_matrix dialog_message dialog_pickfile pialog_printersetup dialog_printjob dialog_read_image dialog_write_image digital_filter dilate dindgen dist double eigenql eigenvec elmhes eof erode erf erfc erfcx execute exp expand_path expint extrac extract_slice f_cvf f_pdf factorial fft file_basename file_dirname file_expand_path file_info file_same file_search file_test file_which filepath findfile findgen finite fix float floor fltarr format_axis_values fstat fulstr fv_test fx_root fz_roots gamma gauss_cvf gauss_pdf gauss2dfit gaussfit gaussint get_drive_list get_kbrd get_screen_size getenv grid_tps grid3 griddata gs_iter hanning hdf_browser hdf_read hilbert hist_2d hist_equal histogram hough hqr ibeta identity idl_validname idlitsys_createtool igamma imaginary indgen int_2d int_3d int_tabulated intarr interpol interpolate invert ioctl ishft julday keword_set krig2d kurtosis kw_test l64indgen label_date label_region ladfit laguerre la_cholmprove la_cholsol la_Determ la_eigenproblem la_eigenql la_eigenvec la_elmhes la_gm_linear_model la_hqr la_invert la_least_square_equality la_least_squares la_linear_equation la_lumprove la_lusol la_trimprove la_trisol leefit legendre linbcg lindgen linfit ll_arc_distance lmfit lmgr lngamma lnp_test locale_get logical_and logical_or logical_true lon64arr lonarr long long64 lsode lu_complex lumprove lusol m_correlate machar make_array map_2points map_image map_patch map_proj_forward map_proj_init map_proj_inverse matrix_multiply matrix_power max md_test mean meanabsdev median memory mesh_clip mesh_decimate mesh_issolid mesh_merge mesh_numtriangles mesh_smooth mesh_surfacearea mesh_validate mesh_volume min min_curve_surf moment morph_close morph_distance morph_gradient morph_histormiss morph_open morph_thin morph_tophat mpeg_open msg_cat_open n_elements n_params n_tags newton norm obj_class obj_isa obj_new obj_valid objarr p_correlate path_sep pcomp pnt_line polar_surface poly poly_2d poly_area poly_fit polyfillv ployshade primes product profile profiles project_vol ptr_new ptr_valid ptrarr qgrid3 qromb qromo qsimp query_bmp query_dicom query_image query_jpeg query_mrsid query_pict query_png query_ppm query_srf query_tiff query_wav r_correlate r_test radon randomn randomu ranks read_ascii read_binary read_bmp read_dicom read_image read_mrsid read_png read_spr read_sylk read_tiff read_wav read_xwd real_part rebin recall_commands recon3 reform region_grow regress replicate reverse rk4 roberts rot rotate round routine_info rs_test s_test savgol search2d search3d sfit shift shmdebug shmvar simplex sin sindgen sinh size skewness smooth sobel sort sph_scat spher_harm spl_init spl_interp spline spline_p sprsab sprsax sprsin sprstp sqrt standardize stddev strarr strcmp strcompress stregex string strjoin strlen strlowcase strmatch strmessage strmid strpos strsplit strtrim strupcase svdfit svsol swap_endian systime t_cvf t_pdf tag_names tan tanh temporary tetra_clip tetra_surface tetra_volume thin timegen tm_test total trace transpose tri_surf trigrid trisol ts_coef ts_diff ts_fcast ts_smooth tvrd uindgen unit uintarr ul64indgen ulindgen ulon64arr ulonarr ulong ulong64 uniq value_locate variance vert_t3d voigt voxel_proj warp_tri watershed where widget_actevix widget_base widget_button widget_combobox widget_draw widget_droplist widget_event widget_info widget_label widget_list widget_propertsheet widget_slider widget_tab widget_table widget_text widget_tree write_sylk wtn xfont xregistered xsq_test' - bistr2 = 'annotate arrow axis bar_plot blas_axpy box_cursor breakpoint byteorder caldata calendar call_method call_procedure catch cd cir_3pnt close color_convert compile_opt constrained_min contour copy_lun cpu create_view cursor cw_animate_getp cw_animate_load cw_animate_run cw_light_editor_get cw_light_editor_set cw_palette_editor_get cw_palette_editor_set define_key define_msgblk define_msgblk_from_file defsysv delvar device dfpmin dissolve dlm_load doc_librar draw_roi efont empty enable_sysrtn erase errplot expand file_chmod file_copy file_delete file_lines file_link file_mkdir file_move file_readlink flick flow3 flush forward_function free_lun funct gamma_ct get_lun grid_input h_eq_ct h_eq_int heap_free heap_gc hls hsv icontour iimage image_cont image_statistics internal_volume iplot isocontour isosurface isurface itcurrent itdelete itgetcurrent itregister itreset ivolume journal la_choldc la_ludc la_svd la_tridc la_triql la_trired linkimage loadct ludc make_dll map_continents map_grid map_proj_info map_set mesh_obj mk_html_help modifyct mpeg_close mpeg_put mpeg_save msg_cat_close msg_cat_compile multi obj_destroy on_error on_ioerror online_help openr openw openu oplot oploterr particle_trace path_cache plot plot_3dbox plot_field ploterr plots point_lun polar_contour polyfill polywarp popd powell printf printd ps_show_fonts psafm pseudo ptr_free pushd qhull rdpix readf read_interfile read_jpeg read_pict read_ppm read_srf read_wave read_x11_bitmap reads readu reduce_colors register_cursor replicate_inplace resolve_all resolve_routine restore save scale3 scale3d set_plot set_shading setenv setup_keys shade_surf shade_surf_irr shade_volume shmmap show3 showfont skip_lun slicer3 slide_image socket spawn sph_4pnt streamline stretch strput struct_assign struct_hide surface surfr svdc swap_enian_inplace t3d tek_color threed time_test2 triangulate triql trired truncate_lun tv tvcrs tvlct tvscl usersym vector_field vel velovect voronoi wait wdelete wf_draw widget_control widget_displaycontextmenu window write_bmp write_image write_jpeg write_nrif write_pict write_png write_ppm write_spr write_srf write_tiff write_wav write_wave writeu wset wshow xbm_edit xdisplayfile xdxf xinteranimate xloadct xmanager xmng_tmpl xmtool xobjview xobjview_rotate xobjview_write_image xpalette xpcolo xplot3d xroi xsurface xvaredit xvolume xyouts zoom zoom_24' - kw = r"\b" + any("keyword", kwstr.split()) + r"\b" - builtin = r"\b" + any("builtin", bistr1.split()+bistr2.split()) + r"\b" - comment = any("comment", [r"\;[^\n]*"]) - number = any("number", - [r"\b[+-]?[0-9]+[lL]?\b", - r"\b[+-]?0[xX][0-9A-Fa-f]+[lL]?\b", - r"\b\.[0-9]d0|\.d0+[lL]?\b", - r"\b[+-]?[0-9]+(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?\b"]) - sqstring = r"(\b[rRuU])?'[^'\\\n]*(\\.[^'\\\n]*)*'?" - dqstring = r'(\b[rRuU])?"[^"\\\n]*(\\.[^"\\\n]*)*"?' - string = any("string", [sqstring, dqstring]) - return "|".join([kw, comment, string, number, builtin, - any("SYNC", [r"\n"])]) - -class IdlSH(GenericSH): - """IDL Syntax Highlighter""" - PROG = re.compile(make_idl_patterns(), re.S|re.I) - - -#============================================================================== -# Diff/Patch highlighter -#============================================================================== - -class DiffSH(BaseSH): - """Simple Diff/Patch Syntax Highlighter Class""" - def highlightBlock(self, text): - text = to_text_string(text) - if text.startswith("+++"): - self.setFormat(0, len(text), self.formats["keyword"]) - elif text.startswith("---"): - self.setFormat(0, len(text), self.formats["keyword"]) - elif text.startswith("+"): - self.setFormat(0, len(text), self.formats["string"]) - elif text.startswith("-"): - self.setFormat(0, len(text), self.formats["number"]) - elif text.startswith("@"): - self.setFormat(0, len(text), self.formats["builtin"]) - - -#============================================================================== -# NSIS highlighter -#============================================================================== - -def make_nsis_patterns(): - "Strongly inspired from idlelib.ColorDelegator.make_pat" - kwstr1 = 'Abort AddBrandingImage AddSize AllowRootDirInstall AllowSkipFiles AutoCloseWindow BGFont BGGradient BrandingText BringToFront Call CallInstDLL Caption ClearErrors CompletedText ComponentText CopyFiles CRCCheck CreateDirectory CreateFont CreateShortCut Delete DeleteINISec DeleteINIStr DeleteRegKey DeleteRegValue DetailPrint DetailsButtonText DirText DirVar DirVerify EnableWindow EnumRegKey EnumRegValue Exec ExecShell ExecWait Exch ExpandEnvStrings File FileBufSize FileClose FileErrorText FileOpen FileRead FileReadByte FileSeek FileWrite FileWriteByte FindClose FindFirst FindNext FindWindow FlushINI Function FunctionEnd GetCurInstType GetCurrentAddress GetDlgItem GetDLLVersion GetDLLVersionLocal GetErrorLevel GetFileTime GetFileTimeLocal GetFullPathName GetFunctionAddress GetInstDirError GetLabelAddress GetTempFileName Goto HideWindow ChangeUI CheckBitmap Icon IfAbort IfErrors IfFileExists IfRebootFlag IfSilent InitPluginsDir InstallButtonText InstallColors InstallDir InstallDirRegKey InstProgressFlags InstType InstTypeGetText InstTypeSetText IntCmp IntCmpU IntFmt IntOp IsWindow LangString LicenseBkColor LicenseData LicenseForceSelection LicenseLangString LicenseText LoadLanguageFile LogSet LogText MessageBox MiscButtonText Name OutFile Page PageCallbacks PageEx PageExEnd Pop Push Quit ReadEnvStr ReadINIStr ReadRegDWORD ReadRegStr Reboot RegDLL Rename ReserveFile Return RMDir SearchPath Section SectionEnd SectionGetFlags SectionGetInstTypes SectionGetSize SectionGetText SectionIn SectionSetFlags SectionSetInstTypes SectionSetSize SectionSetText SendMessage SetAutoClose SetBrandingImage SetCompress SetCompressor SetCompressorDictSize SetCtlColors SetCurInstType SetDatablockOptimize SetDateSave SetDetailsPrint SetDetailsView SetErrorLevel SetErrors SetFileAttributes SetFont SetOutPath SetOverwrite SetPluginUnload SetRebootFlag SetShellVarContext SetSilent ShowInstDetails ShowUninstDetails ShowWindow SilentInstall SilentUnInstall Sleep SpaceTexts StrCmp StrCpy StrLen SubCaption SubSection SubSectionEnd UninstallButtonText UninstallCaption UninstallIcon UninstallSubCaption UninstallText UninstPage UnRegDLL Var VIAddVersionKey VIProductVersion WindowIcon WriteINIStr WriteRegBin WriteRegDWORD WriteRegExpandStr WriteRegStr WriteUninstaller XPStyle' - kwstr2 = 'all alwaysoff ARCHIVE auto both bzip2 components current custom details directory false FILE_ATTRIBUTE_ARCHIVE FILE_ATTRIBUTE_HIDDEN FILE_ATTRIBUTE_NORMAL FILE_ATTRIBUTE_OFFLINE FILE_ATTRIBUTE_READONLY FILE_ATTRIBUTE_SYSTEM FILE_ATTRIBUTE_TEMPORARY force grey HIDDEN hide IDABORT IDCANCEL IDIGNORE IDNO IDOK IDRETRY IDYES ifdiff ifnewer instfiles instfiles lastused leave left level license listonly lzma manual MB_ABORTRETRYIGNORE MB_DEFBUTTON1 MB_DEFBUTTON2 MB_DEFBUTTON3 MB_DEFBUTTON4 MB_ICONEXCLAMATION MB_ICONINFORMATION MB_ICONQUESTION MB_ICONSTOP MB_OK MB_OKCANCEL MB_RETRYCANCEL MB_RIGHT MB_SETFOREGROUND MB_TOPMOST MB_YESNO MB_YESNOCANCEL nevershow none NORMAL off OFFLINE on READONLY right RO show silent silentlog SYSTEM TEMPORARY text textonly true try uninstConfirm windows zlib' - kwstr3 = 'MUI_ABORTWARNING MUI_ABORTWARNING_CANCEL_DEFAULT MUI_ABORTWARNING_TEXT MUI_BGCOLOR MUI_COMPONENTSPAGE_CHECKBITMAP MUI_COMPONENTSPAGE_NODESC MUI_COMPONENTSPAGE_SMALLDESC MUI_COMPONENTSPAGE_TEXT_COMPLIST MUI_COMPONENTSPAGE_TEXT_DESCRIPTION_INFO MUI_COMPONENTSPAGE_TEXT_DESCRIPTION_TITLE MUI_COMPONENTSPAGE_TEXT_INSTTYPE MUI_COMPONENTSPAGE_TEXT_TOP MUI_CUSTOMFUNCTION_ABORT MUI_CUSTOMFUNCTION_GUIINIT MUI_CUSTOMFUNCTION_UNABORT MUI_CUSTOMFUNCTION_UNGUIINIT MUI_DESCRIPTION_TEXT MUI_DIRECTORYPAGE_BGCOLOR MUI_DIRECTORYPAGE_TEXT_DESTINATION MUI_DIRECTORYPAGE_TEXT_TOP MUI_DIRECTORYPAGE_VARIABLE MUI_DIRECTORYPAGE_VERIFYONLEAVE MUI_FINISHPAGE_BUTTON MUI_FINISHPAGE_CANCEL_ENABLED MUI_FINISHPAGE_LINK MUI_FINISHPAGE_LINK_COLOR MUI_FINISHPAGE_LINK_LOCATION MUI_FINISHPAGE_NOAUTOCLOSE MUI_FINISHPAGE_NOREBOOTSUPPORT MUI_FINISHPAGE_REBOOTLATER_DEFAULT MUI_FINISHPAGE_RUN MUI_FINISHPAGE_RUN_FUNCTION MUI_FINISHPAGE_RUN_NOTCHECKED MUI_FINISHPAGE_RUN_PARAMETERS MUI_FINISHPAGE_RUN_TEXT MUI_FINISHPAGE_SHOWREADME MUI_FINISHPAGE_SHOWREADME_FUNCTION MUI_FINISHPAGE_SHOWREADME_NOTCHECKED MUI_FINISHPAGE_SHOWREADME_TEXT MUI_FINISHPAGE_TEXT MUI_FINISHPAGE_TEXT_LARGE MUI_FINISHPAGE_TEXT_REBOOT MUI_FINISHPAGE_TEXT_REBOOTLATER MUI_FINISHPAGE_TEXT_REBOOTNOW MUI_FINISHPAGE_TITLE MUI_FINISHPAGE_TITLE_3LINES MUI_FUNCTION_DESCRIPTION_BEGIN MUI_FUNCTION_DESCRIPTION_END MUI_HEADER_TEXT MUI_HEADER_TRANSPARENT_TEXT MUI_HEADERIMAGE MUI_HEADERIMAGE_BITMAP MUI_HEADERIMAGE_BITMAP_NOSTRETCH MUI_HEADERIMAGE_BITMAP_RTL MUI_HEADERIMAGE_BITMAP_RTL_NOSTRETCH MUI_HEADERIMAGE_RIGHT MUI_HEADERIMAGE_UNBITMAP MUI_HEADERIMAGE_UNBITMAP_NOSTRETCH MUI_HEADERIMAGE_UNBITMAP_RTL MUI_HEADERIMAGE_UNBITMAP_RTL_NOSTRETCH MUI_HWND MUI_ICON MUI_INSTALLCOLORS MUI_INSTALLOPTIONS_DISPLAY MUI_INSTALLOPTIONS_DISPLAY_RETURN MUI_INSTALLOPTIONS_EXTRACT MUI_INSTALLOPTIONS_EXTRACT_AS MUI_INSTALLOPTIONS_INITDIALOG MUI_INSTALLOPTIONS_READ MUI_INSTALLOPTIONS_SHOW MUI_INSTALLOPTIONS_SHOW_RETURN MUI_INSTALLOPTIONS_WRITE MUI_INSTFILESPAGE_ABORTHEADER_SUBTEXT MUI_INSTFILESPAGE_ABORTHEADER_TEXT MUI_INSTFILESPAGE_COLORS MUI_INSTFILESPAGE_FINISHHEADER_SUBTEXT MUI_INSTFILESPAGE_FINISHHEADER_TEXT MUI_INSTFILESPAGE_PROGRESSBAR MUI_LANGDLL_ALLLANGUAGES MUI_LANGDLL_ALWAYSSHOW MUI_LANGDLL_DISPLAY MUI_LANGDLL_INFO MUI_LANGDLL_REGISTRY_KEY MUI_LANGDLL_REGISTRY_ROOT MUI_LANGDLL_REGISTRY_VALUENAME MUI_LANGDLL_WINDOWTITLE MUI_LANGUAGE MUI_LICENSEPAGE_BGCOLOR MUI_LICENSEPAGE_BUTTON MUI_LICENSEPAGE_CHECKBOX MUI_LICENSEPAGE_CHECKBOX_TEXT MUI_LICENSEPAGE_RADIOBUTTONS MUI_LICENSEPAGE_RADIOBUTTONS_TEXT_ACCEPT MUI_LICENSEPAGE_RADIOBUTTONS_TEXT_DECLINE MUI_LICENSEPAGE_TEXT_BOTTOM MUI_LICENSEPAGE_TEXT_TOP MUI_PAGE_COMPONENTS MUI_PAGE_CUSTOMFUNCTION_LEAVE MUI_PAGE_CUSTOMFUNCTION_PRE MUI_PAGE_CUSTOMFUNCTION_SHOW MUI_PAGE_DIRECTORY MUI_PAGE_FINISH MUI_PAGE_HEADER_SUBTEXT MUI_PAGE_HEADER_TEXT MUI_PAGE_INSTFILES MUI_PAGE_LICENSE MUI_PAGE_STARTMENU MUI_PAGE_WELCOME MUI_RESERVEFILE_INSTALLOPTIONS MUI_RESERVEFILE_LANGDLL MUI_SPECIALINI MUI_STARTMENU_GETFOLDER MUI_STARTMENU_WRITE_BEGIN MUI_STARTMENU_WRITE_END MUI_STARTMENUPAGE_BGCOLOR MUI_STARTMENUPAGE_DEFAULTFOLDER MUI_STARTMENUPAGE_NODISABLE MUI_STARTMENUPAGE_REGISTRY_KEY MUI_STARTMENUPAGE_REGISTRY_ROOT MUI_STARTMENUPAGE_REGISTRY_VALUENAME MUI_STARTMENUPAGE_TEXT_CHECKBOX MUI_STARTMENUPAGE_TEXT_TOP MUI_UI MUI_UI_COMPONENTSPAGE_NODESC MUI_UI_COMPONENTSPAGE_SMALLDESC MUI_UI_HEADERIMAGE MUI_UI_HEADERIMAGE_RIGHT MUI_UNABORTWARNING MUI_UNABORTWARNING_CANCEL_DEFAULT MUI_UNABORTWARNING_TEXT MUI_UNCONFIRMPAGE_TEXT_LOCATION MUI_UNCONFIRMPAGE_TEXT_TOP MUI_UNFINISHPAGE_NOAUTOCLOSE MUI_UNFUNCTION_DESCRIPTION_BEGIN MUI_UNFUNCTION_DESCRIPTION_END MUI_UNGETLANGUAGE MUI_UNICON MUI_UNPAGE_COMPONENTS MUI_UNPAGE_CONFIRM MUI_UNPAGE_DIRECTORY MUI_UNPAGE_FINISH MUI_UNPAGE_INSTFILES MUI_UNPAGE_LICENSE MUI_UNPAGE_WELCOME MUI_UNWELCOMEFINISHPAGE_BITMAP MUI_UNWELCOMEFINISHPAGE_BITMAP_NOSTRETCH MUI_UNWELCOMEFINISHPAGE_INI MUI_WELCOMEFINISHPAGE_BITMAP MUI_WELCOMEFINISHPAGE_BITMAP_NOSTRETCH MUI_WELCOMEFINISHPAGE_CUSTOMFUNCTION_INIT MUI_WELCOMEFINISHPAGE_INI MUI_WELCOMEPAGE_TEXT MUI_WELCOMEPAGE_TITLE MUI_WELCOMEPAGE_TITLE_3LINES' - bistr = 'addincludedir addplugindir AndIf cd define echo else endif error execute If ifdef ifmacrodef ifmacrondef ifndef include insertmacro macro macroend onGUIEnd onGUIInit onInit onInstFailed onInstSuccess onMouseOverSection onRebootFailed onSelChange onUserAbort onVerifyInstDir OrIf packhdr system undef verbose warning' - instance = any("instance", [r'\$\{.*?\}', r'\$[A-Za-z0-9\_]*']) - define = any("define", [r"\![^\n]*"]) - comment = any("comment", [r"\;[^\n]*", r"\#[^\n]*", r"\/\*(.*?)\*\/"]) - return make_generic_c_patterns(kwstr1+' '+kwstr2+' '+kwstr3, bistr, - instance=instance, define=define, - comment=comment) - -class NsisSH(CppSH): - """NSIS Syntax Highlighter""" - # Syntax highlighting rules: - PROG = re.compile(make_nsis_patterns(), re.S) - - -#============================================================================== -# gettext highlighter -#============================================================================== - -def make_gettext_patterns(): - "Strongly inspired from idlelib.ColorDelegator.make_pat" - kwstr = 'msgid msgstr' - kw = r"\b" + any("keyword", kwstr.split()) + r"\b" - fuzzy = any("builtin", [r"#,[^\n]*"]) - links = any("normal", [r"#:[^\n]*"]) - comment = any("comment", [r"#[^\n]*"]) - number = any("number", - [r"\b[+-]?[0-9]+[lL]?\b", - r"\b[+-]?0[xX][0-9A-Fa-f]+[lL]?\b", - r"\b[+-]?[0-9]+(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?\b"]) - sqstring = r"(\b[rRuU])?'[^'\\\n]*(\\.[^'\\\n]*)*'?" - dqstring = r'(\b[rRuU])?"[^"\\\n]*(\\.[^"\\\n]*)*"?' - string = any("string", [sqstring, dqstring]) - return "|".join([kw, string, number, fuzzy, links, comment, - any("SYNC", [r"\n"])]) - -class GetTextSH(GenericSH): - """gettext Syntax Highlighter""" - # Syntax highlighting rules: - PROG = re.compile(make_gettext_patterns(), re.S) - -#============================================================================== -# yaml highlighter -#============================================================================== - -def make_yaml_patterns(): - "Strongly inspired from sublime highlighter " - kw = any("keyword", [r":|>|-|\||\[|\]|[A-Za-z][\w\s\-\_ ]+(?=:)"]) - links = any("normal", [r"#:[^\n]*"]) - comment = any("comment", [r"#[^\n]*"]) - number = any("number", - [r"\b[+-]?[0-9]+[lL]?\b", - r"\b[+-]?0[xX][0-9A-Fa-f]+[lL]?\b", - r"\b[+-]?[0-9]+(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?\b"]) - sqstring = r"(\b[rRuU])?'[^'\\\n]*(\\.[^'\\\n]*)*'?" - dqstring = r'(\b[rRuU])?"[^"\\\n]*(\\.[^"\\\n]*)*"?' - string = any("string", [sqstring, dqstring]) - return "|".join([kw, string, number, links, comment, - any("SYNC", [r"\n"])]) - -class YamlSH(GenericSH): - """yaml Syntax Highlighter""" - # Syntax highlighting rules: - PROG = re.compile(make_yaml_patterns(), re.S) - - -#============================================================================== -# HTML highlighter -#============================================================================== - -class BaseWebSH(BaseSH): - """Base class for CSS and HTML syntax highlighters""" - NORMAL = 0 - COMMENT = 1 - - def __init__(self, parent, font=None, color_scheme=None): - BaseSH.__init__(self, parent, font, color_scheme) - - def highlightBlock(self, text): - text = to_text_string(text) - previous_state = self.previousBlockState() - - if previous_state == self.COMMENT: - self.setFormat(0, len(text), self.formats["comment"]) - else: - previous_state = self.NORMAL - self.setFormat(0, len(text), self.formats["normal"]) - - self.setCurrentBlockState(previous_state) - match = self.PROG.search(text) - - match_count = 0 - n_characters = len(text) - # There should never be more matches than characters in the text. - while match and match_count < n_characters: - match_dict = match.groupdict() - for key, value in list(match_dict.items()): - if value: - start, end = match.span(key) - if previous_state == self.COMMENT: - if key == "multiline_comment_end": - self.setCurrentBlockState(self.NORMAL) - self.setFormat(end, len(text), - self.formats["normal"]) - else: - self.setCurrentBlockState(self.COMMENT) - self.setFormat(0, len(text), - self.formats["comment"]) - else: - if key == "multiline_comment_start": - self.setCurrentBlockState(self.COMMENT) - self.setFormat(start, len(text), - self.formats["comment"]) - else: - self.setCurrentBlockState(self.NORMAL) - self.setFormat(start, end-start, - self.formats[key]) - - match = self.PROG.search(text, match.end()) - match_count += 1 - -def make_html_patterns(): - """Strongly inspired from idlelib.ColorDelegator.make_pat """ - tags = any("builtin", [r"<", r"[\?/]?>", r"(?<=<).*?(?=[ >])"]) - keywords = any("keyword", [r" [\w:-]*?(?==)"]) - string = any("string", [r'".*?"']) - comment = any("comment", [r""]) - multiline_comment_start = any("multiline_comment_start", [r""]) - return "|".join([comment, multiline_comment_start, - multiline_comment_end, tags, keywords, string]) - -class HtmlSH(BaseWebSH): - """HTML Syntax Highlighter""" - PROG = re.compile(make_html_patterns(), re.S) - - -#============================================================================== -# Pygments based omni-parser -#============================================================================== - -# IMPORTANT NOTE: -# -------------- -# Do not be tempted to generalize the use of PygmentsSH (that is tempting -# because it would lead to more generic and compact code, and not only in -# this very module) because this generic syntax highlighter is far slower -# than the native ones (all classes above). For example, a Python syntax -# highlighter based on PygmentsSH would be 2 to 3 times slower than the -# current native PythonSH syntax highlighter. - -class PygmentsSH(BaseSH): - """ Generic Pygments syntax highlighter """ - # Store the language name and a ref to the lexer - _lang_name = None - _lexer = None - # Syntax highlighting states (from one text block to another): - NORMAL = 0 - def __init__(self, parent, font=None, color_scheme=None): - # Warning: do not move out those import statements - # (pygments is an optional dependency) - from pygments.lexers import get_lexer_by_name - from pygments.token import (Text, Other, Keyword, Name, String, Number, - Comment, Generic, Token) - # Map Pygments tokens to Spyder tokens - self._tokmap = {Text: "normal", - Generic: "normal", - Other: "normal", - Keyword: "keyword", - Token.Operator: "normal", - Name.Builtin: "builtin", - Name: "normal", - Comment: "comment", - String: "string", - Number: "number"} - # Load Pygments' Lexer - if self._lang_name is not None: - self._lexer = get_lexer_by_name(self._lang_name) - BaseSH.__init__(self, parent, font, color_scheme) - - def get_fmt(self, typ): - """ Get the format code for this type """ - # Exact matches first - for key in self._tokmap: - if typ is key: - return self._tokmap[key] - # Partial (parent-> child) matches - for key in self._tokmap: - if typ in key.subtypes: - return self._tokmap[key] - return 'normal' - - def highlightBlock(self, text): - """ Actually highlight the block """ - text = to_text_string(text) - lextree = self._lexer.get_tokens(text) - ct = 0 - for item in lextree: - typ, val = item - key = self.get_fmt(typ) - start = ct - ct += len(val) - self.setFormat(start, ct-start, self.formats[key]) - -class BatchSH(PygmentsSH): - """Batch highlighter""" - _lang_name = 'bat' - -class IniSH(PygmentsSH): - """INI highlighter""" - _lang_name = 'ini' - -class XmlSH(PygmentsSH): - """XML highlighter""" - _lang_name = 'xml' - -class JsSH(PygmentsSH): - """Javascript highlighter""" - _lang_name = 'js' - -class JsonSH(PygmentsSH): - """Json highlighter""" - _lang_name = 'json' - -class JuliaSH(PygmentsSH): - """Julia highlighter""" - _lang_name = 'julia' - -class CssSH(PygmentsSH): - """CSS Syntax Highlighter""" - _lang_name = 'css' - -class MatlabSH(PygmentsSH): - """Matlab highlighter""" - _lang_name = 'matlab' - - -if __name__ == '__main__': - # Test Python Outline Explorer comment regexps - valid_comments = [ - '# --- First variant', - '#------ 2nd variant', - '### 3rd variant' - ] - invalid_comments = [ - '#---', '#--------', '#--- ', '# -------' - ] - for line in valid_comments: - if not PythonSH.OECOMMENT.match(line): - print("Error matching '%s' as outline comment" % line) - for line in invalid_comments: - if PythonSH.OECOMMENT.match(line): - print("Error: '%s' is matched as outline comment" % line) - diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/widgets/sourcecode/terminal.py spyder-3.0.2+dfsg1/spyder/widgets/sourcecode/terminal.py --- spyder-2.3.8+dfsg1/spyder/widgets/sourcecode/terminal.py 2015-08-24 21:09:46.000000000 +0200 +++ spyder-3.0.2+dfsg1/spyder/widgets/sourcecode/terminal.py 2016-10-25 02:05:23.000000000 +0200 @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- # -# Copyright © 2009-2010 Pierre Raybaut +# Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) +# (see spyder/__init__.py for details) """Terminal emulation tools""" diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/widgets/status.py spyder-3.0.2+dfsg1/spyder/widgets/status.py --- spyder-2.3.8+dfsg1/spyder/widgets/status.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/widgets/status.py 2016-10-25 02:05:23.000000000 +0200 @@ -1,21 +1,23 @@ # -*- coding: utf-8 -*- # -# Copyright © 2012 Pierre Raybaut +# Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) +# (see spyder/__init__.py for details) """Status bar widgets""" +# Standard library imports import os -from spyderlib.qt.QtGui import QWidget, QHBoxLayout, QLabel -from spyderlib.qt.QtCore import QTimer, SIGNAL - -# Local import -from spyderlib.baseconfig import _ -from spyderlib.guiconfig import get_font -from spyderlib.py3compat import to_text_string -from spyderlib import dependencies +# Third party imports +from qtpy.QtCore import QTimer +from qtpy.QtWidgets import QHBoxLayout, QLabel, QWidget + +# Local imports +from spyder import dependencies +from spyder.config.base import _ +from spyder.config.gui import get_font +from spyder.py3compat import to_text_string if not os.name == 'nt': @@ -28,10 +30,10 @@ def __init__(self, parent, statusbar): QWidget.__init__(self, parent) - self.label_font = font = get_font('editor') + self.label_font = font = get_font(option='rich_font') font.setPointSize(self.font().pointSize()) font.setBold(True) - + layout = QHBoxLayout() layout.setContentsMargins(0, 0, 0, 0) self.setLayout(layout) @@ -56,7 +58,7 @@ layout.addSpacing(20) if self.is_supported(): self.timer = QTimer() - self.connect(self.timer, SIGNAL('timeout()'), self.update_label) + self.timer.timeout.connect(self.update_label) self.timer.start(2000) else: self.timer = None @@ -94,11 +96,11 @@ "requires the `psutil` (>=v0.3) library on non-Windows platforms") def import_test(self): """Raise ImportError if feature is not supported""" - from spyderlib.utils.system import memory_usage # analysis:ignore + from spyder.utils.system import memory_usage # analysis:ignore def get_value(self): """Return memory usage""" - from spyderlib.utils.system import memory_usage + from spyder.utils.system import memory_usage return memory_usage() class CPUStatus(BaseTimerStatus): @@ -106,7 +108,7 @@ TIP = _("CPU usage status: requires the `psutil` (>=v0.3) library") def import_test(self): """Raise ImportError if feature is not supported""" - from spyderlib.utils import programs + from spyder.utils import programs if not programs.is_module_installed('psutil', '>=0.2.0'): # The `interval` argument in `psutil.cpu_percent` function # was introduced in v0.2.0 @@ -182,9 +184,10 @@ def test(): - from spyderlib.qt.QtGui import QMainWindow - from spyderlib.utils.qthelpers import qapplication - app = qapplication() + from qtpy.QtWidgets import QMainWindow + from spyder.utils.qthelpers import qapplication + + app = qapplication(test_time=5) win = QMainWindow() win.setWindowTitle("Status widgets test") win.resize(900, 300) @@ -196,6 +199,7 @@ swidgets.append(swidget) win.show() app.exec_() - + + if __name__ == "__main__": test() diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/widgets/tabs.py spyder-3.0.2+dfsg1/spyder/widgets/tabs.py --- spyder-2.3.8+dfsg1/spyder/widgets/tabs.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/widgets/tabs.py 2016-10-25 02:05:23.000000000 +0200 @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- # -# Copyright © 2009-2010 Pierre Raybaut +# Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) +# (see spyder/__init__.py for details) """Tabs widget""" @@ -11,24 +11,30 @@ # pylint: disable=R0911 # pylint: disable=R0201 -from spyderlib.qt.QtGui import (QTabWidget, QMenu, QDrag, QApplication, - QTabBar, QWidget, QHBoxLayout) -from spyderlib.qt.QtCore import SIGNAL, Qt, QPoint, QMimeData, QByteArray - +# Standard library imports import os.path as osp import sys +# Third party imports +from qtpy.QtCore import QByteArray, QMimeData, QPoint, Qt, Signal +from qtpy.QtGui import QDrag +from qtpy.QtWidgets import (QApplication, QHBoxLayout, QMenu, QTabBar, + QTabWidget, QWidget) + # Local imports -from spyderlib.baseconfig import _ -from spyderlib.guiconfig import new_shortcut -from spyderlib.utils.misc import get_common_path -from spyderlib.utils.qthelpers import (add_actions, create_toolbutton, - create_action, get_icon) -from spyderlib.py3compat import PY2, to_text_string +from spyder.config.base import _ +from spyder.config.gui import fixed_shortcut +from spyder.py3compat import PY2, to_text_string +from spyder.utils import icon_manager as ima +from spyder.utils.misc import get_common_path +from spyder.utils.qthelpers import (add_actions, create_action, + create_toolbutton) class TabBar(QTabBar): """Tabs base class with drag and drop support""" + sig_move_tab = Signal((int, int), (str, int, int)) + def __init__(self, parent, ancestor): QTabBar.__init__(self, parent) self.ancestor = ancestor @@ -40,6 +46,7 @@ # Dragging tabs self.__drag_start_pos = QPoint() self.setAcceptDrops(True) + self.setUsesScrollButtons(True) def mousePressEvent(self, event): """Reimplement Qt method""" @@ -96,18 +103,20 @@ # depend on the platform: long for 64bit, int for 32bit. Replacing # by long all the time is not working on some 32bit platforms # (see Issue 1094, Issue 1098) - self.emit(SIGNAL("move_tab(QString,int,int)"), - tabwidget_from, index_from, index_to) + self.sig_move_tab[(str, int, int)].emit(tabwidget_from, index_from, + index_to) event.acceptProposedAction() elif index_from != index_to: - self.emit(SIGNAL("move_tab(int,int)"), index_from, index_to) + self.sig_move_tab.emit(index_from, index_to) event.acceptProposedAction() QTabBar.dropEvent(self, event) class BaseTabs(QTabWidget): """TabWidget with context menu and corner widgets""" + sig_close_tab = Signal(int) + def __init__(self, parent, actions=None, menu=None, corner_widgets=None, menu_use_tooltips=False): QTabWidget.__init__(self, parent) @@ -133,17 +142,16 @@ corner_widgets.setdefault(Qt.TopLeftCorner, []) corner_widgets.setdefault(Qt.TopRightCorner, []) self.browse_button = create_toolbutton(self, - icon=get_icon("browse_tab.png"), + icon=ima.icon('browse_tab'), tip=_("Browse tabs")) self.browse_tabs_menu = QMenu(self) self.browse_button.setMenu(self.browse_tabs_menu) self.browse_button.setPopupMode(self.browse_button.InstantPopup) - self.connect(self.browse_tabs_menu, SIGNAL("aboutToShow()"), - self.update_browse_tabs_menu) + self.browse_tabs_menu.aboutToShow.connect(self.update_browse_tabs_menu) corner_widgets[Qt.TopLeftCorner] += [self.browse_button] self.set_corner_widgets(corner_widgets) - + def update_browse_tabs_menu(self): """Update browse tabs menu""" self.browse_tabs_menu.clear() @@ -170,7 +178,7 @@ if offset <= 3: # Common path is not a path but a drive letter... offset = None - + for index, text in enumerate(names): tab_action = create_action(self, text[offset:], icon=self.tabIcon(index), @@ -179,7 +187,7 @@ tip=self.tabToolTip(index)) tab_action.setChecked(index == self.currentIndex()) self.browse_tabs_menu.addAction(tab_action) - + def set_corner_widgets(self, corner_widgets): """ Set tabs corner widgets @@ -214,6 +222,7 @@ def contextMenuEvent(self, event): """Override Qt method""" + self.setCurrentIndex(self.tabBar().tabAt(event.pos())) if self.menu: self.menu.popup(event.globalPos()) @@ -222,7 +231,7 @@ if event.button() == Qt.MidButton: index = self.tabBar().tabAt(event.pos()) if index >= 0: - self.emit(SIGNAL("close_tab(int)"), index) + self.sig_close_tab.emit(index) event.accept() return QTabWidget.mousePressEvent(self, event) @@ -234,11 +243,17 @@ handled = False if ctrl and self.count() > 0: index = self.currentIndex() - if key == Qt.Key_PageUp and index > 0: - self.setCurrentIndex(index-1) + if key == Qt.Key_PageUp: + if index > 0: + self.setCurrentIndex(index - 1) + else: + self.setCurrentIndex(self.count() - 1) handled = True - elif key == Qt.Key_PageDown and index < self.count()-1: - self.setCurrentIndex(index+1) + elif key == Qt.Key_PageDown: + if index < self.count() - 1: + self.setCurrentIndex(index + 1) + else: + self.setCurrentIndex(0) handled = True if not handled: QTabWidget.keyPressEvent(self, event) @@ -248,38 +263,43 @@ None -> tabs are not closable""" state = func is not None if state: - self.connect(self, SIGNAL("close_tab(int)"), func) + self.sig_close_tab.connect(func) try: # Assuming Qt >= 4.5 QTabWidget.setTabsClosable(self, state) - self.connect(self, SIGNAL("tabCloseRequested(int)"), func) + self.tabCloseRequested.connect(func) except AttributeError: # Workaround for Qt < 4.5 close_button = create_toolbutton(self, triggered=func, - icon=get_icon("fileclose.png"), + icon=ima.icon('fileclose'), tip=_("Close current tab")) self.setCornerWidget(close_button if state else None) class Tabs(BaseTabs): """BaseTabs widget with movable tabs and tab navigation shortcuts""" + # Signals + move_data = Signal(int, int) + move_tab_finished = Signal() + sig_move_tab = Signal(str, str, int, int) + def __init__(self, parent, actions=None, menu=None, corner_widgets=None, menu_use_tooltips=False): BaseTabs.__init__(self, parent, actions, menu, corner_widgets, menu_use_tooltips) tab_bar = TabBar(self, parent) - self.connect(tab_bar, SIGNAL('move_tab(int,int)'), self.move_tab) - self.connect(tab_bar, SIGNAL('move_tab(QString,int,int)'), - self.move_tab_from_another_tabwidget) + tab_bar.sig_move_tab.connect(self.move_tab) + tab_bar.sig_move_tab[(str, int, int)].connect( + self.move_tab_from_another_tabwidget) self.setTabBar(tab_bar) - - new_shortcut("Ctrl+Tab", parent, lambda: self.tab_navigate(1)) - new_shortcut("Shift+Ctrl+Tab", parent, lambda: self.tab_navigate(-1)) - new_shortcut("Ctrl+W", parent, lambda: self.emit(SIGNAL("close_tab(int)"), - self.currentIndex())) - new_shortcut("Ctrl+F4", parent, lambda: self.emit(SIGNAL("close_tab(int)"), - self.currentIndex())) - + + fixed_shortcut("Ctrl+Tab", parent, lambda: self.tab_navigate(1)) + fixed_shortcut("Shift+Ctrl+Tab", parent, lambda: self.tab_navigate(-1)) + fixed_shortcut("Ctrl+W", parent, + lambda: self.sig_close_tab.emit(self.currentIndex())) + fixed_shortcut("Ctrl+F4", parent, + lambda: self.sig_close_tab.emit(self.currentIndex())) + def tab_navigate(self, delta=1): """Ctrl+Tab""" if delta > 0 and self.currentIndex() == self.count()-1: @@ -292,7 +312,7 @@ def move_tab(self, index_from, index_to): """Move tab inside a tabwidget""" - self.emit(SIGNAL('move_data(int,int)'), index_from, index_to) + self.move_data.emit(index_from, index_to) tip, text = self.tabToolTip(index_from), self.tabText(index_from) icon, widget = self.tabIcon(index_from), self.widget(index_from) @@ -303,8 +323,7 @@ self.setTabToolTip(index_to, tip) self.setCurrentWidget(current_widget) - - self.emit(SIGNAL('move_tab_finished()')) + self.move_tab_finished.emit() def move_tab_from_another_tabwidget(self, tabwidget_from, index_from, index_to): @@ -314,5 +333,5 @@ # depend on the platform: long for 64bit, int for 32bit. Replacing # by long all the time is not working on some 32bit platforms # (see Issue 1094, Issue 1098) - self.emit(SIGNAL('move_tab(QString,QString,int,int)'), - tabwidget_from, str(id(self)), index_from, index_to) + self.sig_move_tab.emit(tabwidget_from, str(id(self)), index_from, + index_to) diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/widgets/texteditor.py spyder-3.0.2+dfsg1/spyder/widgets/texteditor.py --- spyder-2.3.8+dfsg1/spyder/widgets/texteditor.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/widgets/texteditor.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,109 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright © 2009-2010 Pierre Raybaut -# Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) - -""" -Text Editor Dialog based on Qt -""" - -from __future__ import print_function - -from spyderlib.qt.QtCore import Qt, SIGNAL, SLOT -from spyderlib.qt.QtGui import QVBoxLayout, QTextEdit, QDialog, QDialogButtonBox - -# Local import -from spyderlib.baseconfig import _ -from spyderlib.guiconfig import get_font -from spyderlib.utils.qthelpers import get_icon -from spyderlib.py3compat import (to_text_string, to_binary_string, - is_binary_string) - - -class TextEditor(QDialog): - """Array Editor Dialog""" - def __init__(self, text, title='', font=None, parent=None, - readonly=False, size=(400, 300)): - QDialog.__init__(self, parent) - - # Destroying the C++ object right after closing the dialog box, - # otherwise it may be garbage-collected in another QThread - # (e.g. the editor's analysis thread in Spyder), thus leading to - # a segmentation fault on UNIX or an application crash on Windows - self.setAttribute(Qt.WA_DeleteOnClose) - - self.text = None - - # Display text as unicode if it comes as bytes, so users see - # its right representation - if is_binary_string(text): - self.is_binary = True - text = to_text_string(text, 'utf8') - else: - self.is_binary = False - - self.layout = QVBoxLayout() - self.setLayout(self.layout) - - # Text edit - self.edit = QTextEdit(parent) - self.connect(self.edit, SIGNAL('textChanged()'), self.text_changed) - self.edit.setReadOnly(readonly) - self.edit.setPlainText(text) - if font is None: - font = get_font('texteditor') - self.edit.setFont(font) - self.layout.addWidget(self.edit) - - # Buttons configuration - buttons = QDialogButtonBox.Ok - if not readonly: - buttons = buttons | QDialogButtonBox.Cancel - bbox = QDialogButtonBox(buttons) - self.connect(bbox, SIGNAL("accepted()"), SLOT("accept()")) - self.connect(bbox, SIGNAL("rejected()"), SLOT("reject()")) - self.layout.addWidget(bbox) - - # Make the dialog act as a window - self.setWindowFlags(Qt.Window) - - self.setWindowIcon(get_icon('edit.png')) - self.setWindowTitle(_("Text editor") + \ - "%s" % (" - "+str(title) if str(title) else "")) - self.resize(size[0], size[1]) - - def text_changed(self): - """Text has changed""" - # Save text as bytes, if it was initially bytes - if self.is_binary: - self.text = to_binary_string(self.edit.toPlainText(), 'utf8') - else: - self.text = to_text_string(self.edit.toPlainText()) - - def get_value(self): - """Return modified text""" - # It is import to avoid accessing Qt C++ object as it has probably - # already been destroyed, due to the Qt.WA_DeleteOnClose attribute - return self.text - - -def test(): - """Text editor demo""" - from spyderlib.utils.qthelpers import qapplication - _app = qapplication() # analysis:ignore - dialog = TextEditor(""" - 01234567890123456789012345678901234567890123456789012345678901234567890123456789 - dedekdh elkd ezd ekjd lekdj elkdfjelfjk e - """) - dialog.show() - if dialog.exec_(): - text = dialog.get_value() - print("Accepted:", text) - dialog = TextEditor(text) - dialog.exec_() - else: - print("Canceled") - -if __name__ == "__main__": - test() \ Kein Zeilenumbruch am Dateiende. diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/widgets/variableexplorer/arrayeditor.py spyder-3.0.2+dfsg1/spyder/widgets/variableexplorer/arrayeditor.py --- spyder-2.3.8+dfsg1/spyder/widgets/variableexplorer/arrayeditor.py 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/widgets/variableexplorer/arrayeditor.py 2016-11-11 05:08:03.000000000 +0100 @@ -0,0 +1,800 @@ +# -*- coding: utf-8 -*- +# +# Copyright © Spyder Project Contributors +# Licensed under the terms of the MIT License +# (see spyder/__init__.py for details) + +""" +NumPy Array Editor Dialog based on Qt +""" + +# pylint: disable=C0103 +# pylint: disable=R0903 +# pylint: disable=R0911 +# pylint: disable=R0201 + +# Standard library imports +from __future__ import print_function + +# Third party imports +from qtpy.compat import from_qvariant, to_qvariant +from qtpy.QtCore import (QAbstractTableModel, QItemSelection, + QItemSelectionRange, QModelIndex, Qt, Slot) +from qtpy.QtGui import QColor, QCursor, QDoubleValidator, QKeySequence +from qtpy.QtWidgets import (QAbstractItemDelegate, QApplication, QCheckBox, + QComboBox, QDialog, QDialogButtonBox, QGridLayout, + QHBoxLayout, QInputDialog, QItemDelegate, QLabel, + QLineEdit, QMenu, QMessageBox, QPushButton, + QSpinBox, QStackedWidget, QTableView, QVBoxLayout, + QWidget) +import numpy as np + +# Local imports +from spyder.config.base import _ +from spyder.config.fonts import DEFAULT_SMALL_DELTA +from spyder.config.gui import get_font, fixed_shortcut +from spyder.py3compat import (io, is_binary_string, is_string, + is_text_string, PY3, to_binary_string, + to_text_string) +from spyder.utils import icon_manager as ima +from spyder.utils.qthelpers import add_actions, create_action, keybinding + + +# Note: string and unicode data types will be formatted with '%s' (see below) +SUPPORTED_FORMATS = { + 'single': '%.3f', + 'double': '%.3f', + 'float_': '%.3f', + 'longfloat': '%.3f', + 'float16': '%.3f', + 'float32': '%.3f', + 'float64': '%.3f', + 'float96': '%.3f', + 'float128': '%.3f', + 'csingle': '%r', + 'complex_': '%r', + 'clongfloat': '%r', + 'complex64': '%r', + 'complex128': '%r', + 'complex192': '%r', + 'complex256': '%r', + 'byte': '%d', + 'bytes8': '%s', + 'short': '%d', + 'intc': '%d', + 'int_': '%d', + 'longlong': '%d', + 'intp': '%d', + 'int8': '%d', + 'int16': '%d', + 'int32': '%d', + 'int64': '%d', + 'ubyte': '%d', + 'ushort': '%d', + 'uintc': '%d', + 'uint': '%d', + 'ulonglong': '%d', + 'uintp': '%d', + 'uint8': '%d', + 'uint16': '%d', + 'uint32': '%d', + 'uint64': '%d', + 'bool_': '%r', + 'bool8': '%r', + 'bool': '%r', + } + + +LARGE_SIZE = 5e5 +LARGE_NROWS = 1e5 +LARGE_COLS = 60 + + +#============================================================================== +# Utility functions +#============================================================================== +def is_float(dtype): + """Return True if datatype dtype is a float kind""" + return ('float' in dtype.name) or dtype.name in ['single', 'double'] + + +def is_number(dtype): + """Return True is datatype dtype is a number kind""" + return is_float(dtype) or ('int' in dtype.name) or ('long' in dtype.name) \ + or ('short' in dtype.name) + + +def get_idx_rect(index_list): + """Extract the boundaries from a list of indexes""" + rows, cols = list(zip(*[(i.row(), i.column()) for i in index_list])) + return ( min(rows), max(rows), min(cols), max(cols) ) + + +#============================================================================== +# Main classes +#============================================================================== +class ArrayModel(QAbstractTableModel): + """Array Editor Table Model""" + + ROWS_TO_LOAD = 500 + COLS_TO_LOAD = 40 + + def __init__(self, data, format="%.3f", xlabels=None, ylabels=None, + readonly=False, parent=None): + QAbstractTableModel.__init__(self) + + self.dialog = parent + self.changes = {} + self.xlabels = xlabels + self.ylabels = ylabels + self.readonly = readonly + self.test_array = np.array([0], dtype=data.dtype) + + # for complex numbers, shading will be based on absolute value + # but for all other types it will be the real part + if data.dtype in (np.complex64, np.complex128): + self.color_func = np.abs + else: + self.color_func = np.real + + # Backgroundcolor settings + huerange = [.66, .99] # Hue + self.sat = .7 # Saturation + self.val = 1. # Value + self.alp = .6 # Alpha-channel + + self._data = data + self._format = format + + self.total_rows = self._data.shape[0] + self.total_cols = self._data.shape[1] + size = self.total_rows * self.total_cols + + try: + self.vmin = np.nanmin(self.color_func(data)) + self.vmax = np.nanmax(self.color_func(data)) + if self.vmax == self.vmin: + self.vmin -= 1 + self.hue0 = huerange[0] + self.dhue = huerange[1]-huerange[0] + self.bgcolor_enabled = True + except TypeError: + self.vmin = None + self.vmax = None + self.hue0 = None + self.dhue = None + self.bgcolor_enabled = False + + # Use paging when the total size, number of rows or number of + # columns is too large + if size > LARGE_SIZE: + self.rows_loaded = self.ROWS_TO_LOAD + self.cols_loaded = self.COLS_TO_LOAD + else: + if self.total_rows > LARGE_NROWS: + self.rows_loaded = self.ROWS_TO_LOAD + else: + self.rows_loaded = self.total_rows + if self.total_cols > LARGE_COLS: + self.cols_loaded = self.COLS_TO_LOAD + else: + self.cols_loaded = self.total_cols + + def get_format(self): + """Return current format""" + # Avoid accessing the private attribute _format from outside + return self._format + + def get_data(self): + """Return data""" + return self._data + + def set_format(self, format): + """Change display format""" + self._format = format + self.reset() + + def columnCount(self, qindex=QModelIndex()): + """Array column number""" + if self.total_cols <= self.cols_loaded: + return self.total_cols + else: + return self.cols_loaded + + def rowCount(self, qindex=QModelIndex()): + """Array row number""" + if self.total_rows <= self.rows_loaded: + return self.total_rows + else: + return self.rows_loaded + + def can_fetch_more(self, rows=False, columns=False): + if rows: + if self.total_rows > self.rows_loaded: + return True + else: + return False + if columns: + if self.total_cols > self.cols_loaded: + return True + else: + return False + + def fetch_more(self, rows=False, columns=False): + if self.can_fetch_more(rows=rows): + reminder = self.total_rows - self.rows_loaded + items_to_fetch = min(reminder, self.ROWS_TO_LOAD) + self.beginInsertRows(QModelIndex(), self.rows_loaded, + self.rows_loaded + items_to_fetch - 1) + self.rows_loaded += items_to_fetch + self.endInsertRows() + if self.can_fetch_more(columns=columns): + reminder = self.total_cols - self.cols_loaded + items_to_fetch = min(reminder, self.COLS_TO_LOAD) + self.beginInsertColumns(QModelIndex(), self.cols_loaded, + self.cols_loaded + items_to_fetch - 1) + self.cols_loaded += items_to_fetch + self.endInsertColumns() + + def bgcolor(self, state): + """Toggle backgroundcolor""" + self.bgcolor_enabled = state > 0 + self.reset() + + def get_value(self, index): + i = index.row() + j = index.column() + return self.changes.get((i, j), self._data[i, j]) + + def data(self, index, role=Qt.DisplayRole): + """Cell content""" + if not index.isValid(): + return to_qvariant() + value = self.get_value(index) + if is_binary_string(value): + try: + value = to_text_string(value, 'utf8') + except: + pass + if role == Qt.DisplayRole: + if value is np.ma.masked: + return '' + else: + return to_qvariant(self._format % value) + elif role == Qt.TextAlignmentRole: + return to_qvariant(int(Qt.AlignCenter|Qt.AlignVCenter)) + elif role == Qt.BackgroundColorRole and self.bgcolor_enabled \ + and value is not np.ma.masked: + hue = self.hue0+\ + self.dhue*(self.vmax-self.color_func(value)) \ + /(self.vmax-self.vmin) + hue = float(np.abs(hue)) + color = QColor.fromHsvF(hue, self.sat, self.val, self.alp) + return to_qvariant(color) + elif role == Qt.FontRole: + return to_qvariant(get_font(font_size_delta=DEFAULT_SMALL_DELTA)) + return to_qvariant() + + def setData(self, index, value, role=Qt.EditRole): + """Cell content change""" + if not index.isValid() or self.readonly: + return False + i = index.row() + j = index.column() + value = from_qvariant(value, str) + dtype = self._data.dtype.name + if dtype == "bool": + try: + val = bool(float(value)) + except ValueError: + val = value.lower() == "true" + elif dtype.startswith("string") or dtype.startswith("bytes"): + val = to_binary_string(value, 'utf8') + elif dtype.startswith("unicode") or dtype.startswith("str"): + val = to_text_string(value) + else: + if value.lower().startswith('e') or value.lower().endswith('e'): + return False + try: + val = complex(value) + if not val.imag: + val = val.real + except ValueError as e: + QMessageBox.critical(self.dialog, "Error", + "Value error: %s" % str(e)) + return False + try: + self.test_array[0] = val # will raise an Exception eventually + except OverflowError as e: + print(type(e.message)) + QMessageBox.critical(self.dialog, "Error", + "Overflow error: %s" % e.message) + return False + + # Add change to self.changes + self.changes[(i, j)] = val + self.dataChanged.emit(index, index) + if not is_string(val): + if val > self.vmax: + self.vmax = val + if val < self.vmin: + self.vmin = val + return True + + def flags(self, index): + """Set editable flag""" + if not index.isValid(): + return Qt.ItemIsEnabled + return Qt.ItemFlags(QAbstractTableModel.flags(self, index)| + Qt.ItemIsEditable) + + def headerData(self, section, orientation, role=Qt.DisplayRole): + """Set header data""" + if role != Qt.DisplayRole: + return to_qvariant() + labels = self.xlabels if orientation == Qt.Horizontal else self.ylabels + if labels is None: + return to_qvariant(int(section)) + else: + return to_qvariant(labels[section]) + + def reset(self): + self.beginResetModel() + self.endResetModel() + + +class ArrayDelegate(QItemDelegate): + """Array Editor Item Delegate""" + def __init__(self, dtype, parent=None): + QItemDelegate.__init__(self, parent) + self.dtype = dtype + + def createEditor(self, parent, option, index): + """Create editor widget""" + model = index.model() + value = model.get_value(index) + if model._data.dtype.name == "bool": + value = not value + model.setData(index, to_qvariant(value)) + return + elif value is not np.ma.masked: + editor = QLineEdit(parent) + editor.setFont(get_font(font_size_delta=DEFAULT_SMALL_DELTA)) + editor.setAlignment(Qt.AlignCenter) + if is_number(self.dtype): + editor.setValidator(QDoubleValidator(editor)) + editor.returnPressed.connect(self.commitAndCloseEditor) + return editor + + def commitAndCloseEditor(self): + """Commit and close editor""" + editor = self.sender() + # Avoid a segfault with PyQt5. Variable value won't be changed + # but at least Spyder won't crash. It seems generated by a + # bug in sip. See + # http://comments.gmane.org/gmane.comp.python.pyqt-pykde/26544 + try: + self.commitData.emit(editor) + except AttributeError: + pass + self.closeEditor.emit(editor, QAbstractItemDelegate.NoHint) + + def setEditorData(self, editor, index): + """Set editor widget's data""" + text = from_qvariant(index.model().data(index, Qt.DisplayRole), str) + editor.setText(text) + + +#TODO: Implement "Paste" (from clipboard) feature +class ArrayView(QTableView): + """Array view class""" + def __init__(self, parent, model, dtype, shape): + QTableView.__init__(self, parent) + + self.setModel(model) + self.setItemDelegate(ArrayDelegate(dtype, self)) + total_width = 0 + for k in range(shape[1]): + total_width += self.columnWidth(k) + self.viewport().resize(min(total_width, 1024), self.height()) + self.shape = shape + self.menu = self.setup_menu() + fixed_shortcut(QKeySequence.Copy, self, self.copy) + self.horizontalScrollBar().valueChanged.connect( + lambda val: self.load_more_data(val, columns=True)) + self.verticalScrollBar().valueChanged.connect( + lambda val: self.load_more_data(val, rows=True)) + + def load_more_data(self, value, rows=False, columns=False): + old_selection = self.selectionModel().selection() + old_rows_loaded = old_cols_loaded = None + + if rows and value == self.verticalScrollBar().maximum(): + old_rows_loaded = self.model().rows_loaded + self.model().fetch_more(rows=rows) + + if columns and value == self.horizontalScrollBar().maximum(): + old_cols_loaded = self.model().cols_loaded + self.model().fetch_more(columns=columns) + + if old_rows_loaded is not None or old_cols_loaded is not None: + # if we've changed anything, update selection + new_selection = QItemSelection() + for part in old_selection: + top = part.top() + bottom = part.bottom() + if (old_rows_loaded is not None and + top == 0 and bottom == (old_rows_loaded-1)): + # complete column selected (so expand it to match updated range) + bottom = self.model().rows_loaded-1 + left = part.left() + right = part.right() + if (old_cols_loaded is not None + and left == 0 and right == (old_cols_loaded-1)): + # compete row selected (so expand it to match updated range) + right = self.model().cols_loaded-1 + top_left = self.model().index(top, left) + bottom_right = self.model().index(bottom, right) + part = QItemSelectionRange(top_left, bottom_right) + new_selection.append(part) + self.selectionModel().select(new_selection, self.selectionModel().ClearAndSelect) + + + def resize_to_contents(self): + """Resize cells to contents""" + QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) + self.resizeColumnsToContents() + self.model().fetch_more(columns=True) + self.resizeColumnsToContents() + QApplication.restoreOverrideCursor() + + def setup_menu(self): + """Setup context menu""" + self.copy_action = create_action(self, _('Copy'), + shortcut=keybinding('Copy'), + icon=ima.icon('editcopy'), + triggered=self.copy, + context=Qt.WidgetShortcut) + menu = QMenu(self) + add_actions(menu, [self.copy_action, ]) + return menu + + def contextMenuEvent(self, event): + """Reimplement Qt method""" + self.menu.popup(event.globalPos()) + event.accept() + + def keyPressEvent(self, event): + """Reimplement Qt method""" + if event == QKeySequence.Copy: + self.copy() + else: + QTableView.keyPressEvent(self, event) + + def _sel_to_text(self, cell_range): + """Copy an array portion to a unicode string""" + if not cell_range: + return + row_min, row_max, col_min, col_max = get_idx_rect(cell_range) + if col_min == 0 and col_max == (self.model().cols_loaded-1): + # we've selected a whole column. It isn't possible to + # select only the first part of a column without loading more, + # so we can treat it as intentional and copy the whole thing + col_max = self.model().total_cols-1 + if row_min == 0 and row_max == (self.model().rows_loaded-1): + row_max = self.model().total_rows-1 + + _data = self.model().get_data() + if PY3: + output = io.BytesIO() + else: + output = io.StringIO() + try: + np.savetxt(output, _data[row_min:row_max+1, col_min:col_max+1], + delimiter='\t') + except: + QMessageBox.warning(self, _("Warning"), + _("It was not possible to copy values for " + "this array")) + return + contents = output.getvalue().decode('utf-8') + output.close() + return contents + + @Slot() + def copy(self): + """Copy text to clipboard""" + cliptxt = self._sel_to_text( self.selectedIndexes() ) + clipboard = QApplication.clipboard() + clipboard.setText(cliptxt) + + +class ArrayEditorWidget(QWidget): + def __init__(self, parent, data, readonly=False, + xlabels=None, ylabels=None): + QWidget.__init__(self, parent) + self.data = data + self.old_data_shape = None + if len(self.data.shape) == 1: + self.old_data_shape = self.data.shape + self.data.shape = (self.data.shape[0], 1) + elif len(self.data.shape) == 0: + self.old_data_shape = self.data.shape + self.data.shape = (1, 1) + + format = SUPPORTED_FORMATS.get(data.dtype.name, '%s') + self.model = ArrayModel(self.data, format=format, xlabels=xlabels, + ylabels=ylabels, readonly=readonly, parent=self) + self.view = ArrayView(self, self.model, data.dtype, data.shape) + + btn_layout = QHBoxLayout() + btn_layout.setAlignment(Qt.AlignLeft) + btn = QPushButton(_( "Format")) + # disable format button for int type + btn.setEnabled(is_float(data.dtype)) + btn_layout.addWidget(btn) + btn.clicked.connect(self.change_format) + btn = QPushButton(_( "Resize")) + btn_layout.addWidget(btn) + btn.clicked.connect(self.view.resize_to_contents) + bgcolor = QCheckBox(_( 'Background color')) + bgcolor.setChecked(self.model.bgcolor_enabled) + bgcolor.setEnabled(self.model.bgcolor_enabled) + bgcolor.stateChanged.connect(self.model.bgcolor) + btn_layout.addWidget(bgcolor) + + layout = QVBoxLayout() + layout.addWidget(self.view) + layout.addLayout(btn_layout) + self.setLayout(layout) + + def accept_changes(self): + """Accept changes""" + for (i, j), value in list(self.model.changes.items()): + self.data[i, j] = value + if self.old_data_shape is not None: + self.data.shape = self.old_data_shape + + def reject_changes(self): + """Reject changes""" + if self.old_data_shape is not None: + self.data.shape = self.old_data_shape + + def change_format(self): + """Change display format""" + format, valid = QInputDialog.getText(self, _( 'Format'), + _( "Float formatting"), + QLineEdit.Normal, self.model.get_format()) + if valid: + format = str(format) + try: + format % 1.1 + except: + QMessageBox.critical(self, _("Error"), + _("Format (%s) is incorrect") % format) + return + self.model.set_format(format) + + +class ArrayEditor(QDialog): + """Array Editor Dialog""" + def __init__(self, parent=None): + QDialog.__init__(self, parent) + + # Destroying the C++ object right after closing the dialog box, + # otherwise it may be garbage-collected in another QThread + # (e.g. the editor's analysis thread in Spyder), thus leading to + # a segmentation fault on UNIX or an application crash on Windows + self.setAttribute(Qt.WA_DeleteOnClose) + + self.data = None + self.arraywidget = None + self.stack = None + self.layout = None + # Values for 3d array editor + self.dim_indexes = [{}, {}, {}] + self.last_dim = 0 # Adjust this for changing the startup dimension + + def setup_and_check(self, data, title='', readonly=False, + xlabels=None, ylabels=None): + """ + Setup ArrayEditor: + return False if data is not supported, True otherwise + """ + self.data = data + self.data.flags.writeable = True + is_record_array = data.dtype.names is not None + is_masked_array = isinstance(data, np.ma.MaskedArray) + if data.size == 0: + self.error(_("Array is empty")) + return False + if data.ndim > 3: + self.error(_("Arrays with more than 3 dimensions are not supported")) + return False + if xlabels is not None and len(xlabels) != self.data.shape[1]: + self.error(_("The 'xlabels' argument length do no match array " + "column number")) + return False + if ylabels is not None and len(ylabels) != self.data.shape[0]: + self.error(_("The 'ylabels' argument length do no match array row " + "number")) + return False + if not is_record_array: + dtn = data.dtype.name + if dtn not in SUPPORTED_FORMATS and not dtn.startswith('str') \ + and not dtn.startswith('unicode'): + arr = _("%s arrays") % data.dtype.name + self.error(_("%s are currently not supported") % arr) + return False + + self.layout = QGridLayout() + self.setLayout(self.layout) + self.setWindowIcon(ima.icon('arredit')) + if title: + title = to_text_string(title) + " - " + _("NumPy array") + else: + title = _("Array editor") + if readonly: + title += ' (' + _('read only') + ')' + self.setWindowTitle(title) + self.resize(600, 500) + + # Stack widget + self.stack = QStackedWidget(self) + if is_record_array: + for name in data.dtype.names: + self.stack.addWidget(ArrayEditorWidget(self, data[name], + readonly, xlabels, ylabels)) + elif is_masked_array: + self.stack.addWidget(ArrayEditorWidget(self, data, readonly, + xlabels, ylabels)) + self.stack.addWidget(ArrayEditorWidget(self, data.data, readonly, + xlabels, ylabels)) + self.stack.addWidget(ArrayEditorWidget(self, data.mask, readonly, + xlabels, ylabels)) + elif data.ndim == 3: + pass + else: + self.stack.addWidget(ArrayEditorWidget(self, data, readonly, + xlabels, ylabels)) + self.arraywidget = self.stack.currentWidget() + self.stack.currentChanged.connect(self.current_widget_changed) + self.layout.addWidget(self.stack, 1, 0) + + # Buttons configuration + btn_layout = QHBoxLayout() + if is_record_array or is_masked_array or data.ndim == 3: + if is_record_array: + btn_layout.addWidget(QLabel(_("Record array fields:"))) + names = [] + for name in data.dtype.names: + field = data.dtype.fields[name] + text = name + if len(field) >= 3: + title = field[2] + if not is_text_string(title): + title = repr(title) + text += ' - '+title + names.append(text) + else: + names = [_('Masked data'), _('Data'), _('Mask')] + if data.ndim == 3: + # QSpinBox + self.index_spin = QSpinBox(self, keyboardTracking=False) + self.index_spin.valueChanged.connect(self.change_active_widget) + # QComboBox + names = [str(i) for i in range(3)] + ra_combo = QComboBox(self) + ra_combo.addItems(names) + ra_combo.currentIndexChanged.connect(self.current_dim_changed) + # Adding the widgets to layout + label = QLabel(_("Axis:")) + btn_layout.addWidget(label) + btn_layout.addWidget(ra_combo) + self.shape_label = QLabel() + btn_layout.addWidget(self.shape_label) + label = QLabel(_("Index:")) + btn_layout.addWidget(label) + btn_layout.addWidget(self.index_spin) + self.slicing_label = QLabel() + btn_layout.addWidget(self.slicing_label) + # set the widget to display when launched + self.current_dim_changed(self.last_dim) + else: + ra_combo = QComboBox(self) + ra_combo.currentIndexChanged.connect(self.stack.setCurrentIndex) + ra_combo.addItems(names) + btn_layout.addWidget(ra_combo) + if is_masked_array: + label = QLabel(_("Warning: changes are applied separately")) + label.setToolTip(_("For performance reasons, changes applied "\ + "to masked array won't be reflected in "\ + "array's data (and vice-versa).")) + btn_layout.addWidget(label) + btn_layout.addStretch() + bbox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) + bbox.accepted.connect(self.accept) + bbox.rejected.connect(self.reject) + btn_layout.addWidget(bbox) + self.layout.addLayout(btn_layout, 2, 0) + + self.setMinimumSize(400, 300) + + # Make the dialog act as a window + self.setWindowFlags(Qt.Window) + + return True + + def current_widget_changed(self, index): + self.arraywidget = self.stack.widget(index) + + def change_active_widget(self, index): + """ + This is implemented for handling negative values in index for + 3d arrays, to give the same behavior as slicing + """ + string_index = [':']*3 + string_index[self.last_dim] = '%i' + self.slicing_label.setText((r"Slicing: [" + ", ".join(string_index) + + "]") % index) + if index < 0: + data_index = self.data.shape[self.last_dim] + index + else: + data_index = index + slice_index = [slice(None)]*3 + slice_index[self.last_dim] = data_index + + stack_index = self.dim_indexes[self.last_dim].get(data_index) + if stack_index == None: + stack_index = self.stack.count() + self.stack.addWidget(ArrayEditorWidget(self, + self.data[slice_index])) + self.dim_indexes[self.last_dim][data_index] = stack_index + self.stack.update() + self.stack.setCurrentIndex(stack_index) + + def current_dim_changed(self, index): + """ + This change the active axis the array editor is plotting over + in 3D + """ + self.last_dim = index + string_size = ['%i']*3 + string_size[index] = '%i' + self.shape_label.setText(('Shape: (' + ', '.join(string_size) + + ') ') % self.data.shape) + if self.index_spin.value() != 0: + self.index_spin.setValue(0) + else: + # this is done since if the value is currently 0 it does not emit + # currentIndexChanged(int) + self.change_active_widget(0) + self.index_spin.setRange(-self.data.shape[index], + self.data.shape[index]-1) + + @Slot() + def accept(self): + """Reimplement Qt method""" + for index in range(self.stack.count()): + self.stack.widget(index).accept_changes() + QDialog.accept(self) + + def get_value(self): + """Return modified array -- this is *not* a copy""" + # It is import to avoid accessing Qt C++ object as it has probably + # already been destroyed, due to the Qt.WA_DeleteOnClose attribute + return self.data + + def error(self, message): + """An error occured, closing the dialog box""" + QMessageBox.critical(self, _("Array editor"), message) + self.setAttribute(Qt.WA_DeleteOnClose) + self.reject() + + @Slot() + def reject(self): + """Reimplement Qt method""" + if self.arraywidget is not None: + for index in range(self.stack.count()): + self.stack.widget(index).reject_changes() + QDialog.reject(self) diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/widgets/variableexplorer/collectionseditor.py spyder-3.0.2+dfsg1/spyder/widgets/variableexplorer/collectionseditor.py --- spyder-2.3.8+dfsg1/spyder/widgets/variableexplorer/collectionseditor.py 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/widgets/variableexplorer/collectionseditor.py 2016-11-17 04:39:40.000000000 +0100 @@ -0,0 +1,1493 @@ +# -*- coding: utf-8 -*- +# +# Copyright © Spyder Project Contributors +# Licensed under the terms of the MIT License +# (see spyder/__init__.py for details) + +""" +Collections (i.e. dictionary, list and tuple) editor widget and dialog +""" + +#TODO: Multiple selection: open as many editors (array/dict/...) as necessary, +# at the same time + +# pylint: disable=C0103 +# pylint: disable=R0903 +# pylint: disable=R0911 +# pylint: disable=R0201 + +# Standard library imports +from __future__ import print_function +import datetime +import gc +import sys +import warnings + +# Third party imports +from qtpy.compat import getsavefilename, to_qvariant +from qtpy.QtCore import (QAbstractTableModel, QDateTime, QModelIndex, Qt, + Signal, Slot) +from qtpy.QtGui import QColor, QKeySequence +from qtpy.QtWidgets import (QAbstractItemDelegate, QApplication, QDateEdit, + QDateTimeEdit, QDialog, QDialogButtonBox, + QInputDialog, QItemDelegate, QLineEdit, QMenu, + QMessageBox, QTableView, QVBoxLayout, QWidget) + +# Local import +from spyder.config.base import _ +from spyder.config.fonts import DEFAULT_SMALL_DELTA +from spyder.config.gui import get_font +from spyder.py3compat import (io, is_binary_string, is_text_string, + getcwd, PY3, to_text_string) +from spyder.utils import icon_manager as ima +from spyder.utils.misc import fix_reference_name +from spyder.utils.qthelpers import (add_actions, create_action, + mimedata2url) +from spyder.widgets.variableexplorer.importwizard import ImportWizard +from spyder.widgets.variableexplorer.texteditor import TextEditor +from spyder.widgets.variableexplorer.utils import ( + array, DataFrame, display_to_value, FakeObject, get_color_name, + get_human_readable_type, get_size, Image, is_editable_type, is_known_type, + MaskedArray, ndarray, np_savetxt, Series, sort_against, try_to_eval, + unsorted_unique, value_to_display,) + +if ndarray is not FakeObject: + from spyder.widgets.variableexplorer.arrayeditor import ArrayEditor + +if DataFrame is not FakeObject: + from spyder.widgets.variableexplorer.dataframeeditor import DataFrameEditor + + +LARGE_NROWS = 100 + + +class ProxyObject(object): + """Dictionary proxy to an unknown object""" + def __init__(self, obj): + self.__obj__ = obj + + def __len__(self): + return len(dir(self.__obj__)) + + def __getitem__(self, key): + return getattr(self.__obj__, key) + + def __setitem__(self, key, value): + setattr(self.__obj__, key, value) + + +class ReadOnlyCollectionsModel(QAbstractTableModel): + """CollectionsEditor Read-Only Table Model""" + ROWS_TO_LOAD = 50 + + def __init__(self, parent, data, title="", names=False, + minmax=False, remote=False): + QAbstractTableModel.__init__(self, parent) + if data is None: + data = {} + self.names = names + self.minmax = minmax + self.remote = remote + self.header0 = None + self._data = None + self.total_rows = None + self.showndata = None + self.keys = None + self.title = to_text_string(title) # in case title is not a string + if self.title: + self.title = self.title + ' - ' + self.sizes = [] + self.types = [] + self.set_data(data) + + def get_data(self): + """Return model data""" + return self._data + + def set_data(self, data, coll_filter=None): + """Set model data""" + self._data = data + + if coll_filter is not None and not self.remote and \ + isinstance(data, (tuple, list, dict)): + data = coll_filter(data) + self.showndata = data + + self.header0 = _("Index") + if self.names: + self.header0 = _("Name") + if isinstance(data, tuple): + self.keys = list(range(len(data))) + self.title += _("Tuple") + elif isinstance(data, list): + self.keys = list(range(len(data))) + self.title += _("List") + elif isinstance(data, dict): + self.keys = list(data.keys()) + self.title += _("Dictionary") + if not self.names: + self.header0 = _("Key") + else: + self.keys = dir(data) + self._data = data = self.showndata = ProxyObject(data) + self.title += _("Object") + if not self.names: + self.header0 = _("Attribute") + + self.title += ' ('+str(len(self.keys))+' '+ _("elements")+')' + + self.total_rows = len(self.keys) + if self.total_rows > LARGE_NROWS: + self.rows_loaded = self.ROWS_TO_LOAD + else: + self.rows_loaded = self.total_rows + + self.set_size_and_type() + self.reset() + + def set_size_and_type(self, start=None, stop=None): + data = self._data + + if start is None and stop is None: + start = 0 + stop = self.rows_loaded + fetch_more = False + else: + fetch_more = True + + if self.remote: + sizes = [ data[self.keys[index]]['size'] + for index in range(start, stop) ] + types = [ data[self.keys[index]]['type'] + for index in range(start, stop) ] + else: + sizes = [ get_size(data[self.keys[index]]) + for index in range(start, stop) ] + types = [ get_human_readable_type(data[self.keys[index]]) + for index in range(start, stop) ] + + if fetch_more: + self.sizes = self.sizes + sizes + self.types = self.types + types + else: + self.sizes = sizes + self.types = types + + def sort(self, column, order=Qt.AscendingOrder): + """Overriding sort method""" + reverse = (order==Qt.DescendingOrder) + if column == 0: + self.sizes = sort_against(self.sizes, self.keys, reverse) + self.types = sort_against(self.types, self.keys, reverse) + try: + self.keys.sort(reverse=reverse) + except: + pass + elif column == 1: + self.keys = sort_against(self.keys, self.types, reverse) + self.sizes = sort_against(self.sizes, self.types, reverse) + try: + self.types.sort(reverse=reverse) + except: + pass + elif column == 2: + self.keys = sort_against(self.keys, self.sizes, reverse) + self.types = sort_against(self.types, self.sizes, reverse) + try: + self.sizes.sort(reverse=reverse) + except: + pass + elif column == 3: + self.keys = sort_against(self.keys, self.sizes, reverse) + self.types = sort_against(self.types, self.sizes, reverse) + try: + self.sizes.sort(reverse=reverse) + except: + pass + elif column == 4: + values = [self._data[key] for key in self.keys] + self.keys = sort_against(self.keys, values, reverse) + self.sizes = sort_against(self.sizes, values, reverse) + self.types = sort_against(self.types, values, reverse) + self.beginResetModel() + self.endResetModel() + + def columnCount(self, qindex=QModelIndex()): + """Array column number""" + return 4 + + def rowCount(self, index=QModelIndex()): + """Array row number""" + if self.total_rows <= self.rows_loaded: + return self.total_rows + else: + return self.rows_loaded + + def canFetchMore(self, index=QModelIndex()): + if self.total_rows > self.rows_loaded: + return True + else: + return False + + def fetchMore(self, index=QModelIndex()): + reminder = self.total_rows - self.rows_loaded + items_to_fetch = min(reminder, self.ROWS_TO_LOAD) + self.set_size_and_type(self.rows_loaded, + self.rows_loaded + items_to_fetch) + self.beginInsertRows(QModelIndex(), self.rows_loaded, + self.rows_loaded + items_to_fetch - 1) + self.rows_loaded += items_to_fetch + self.endInsertRows() + + def get_index_from_key(self, key): + try: + return self.createIndex(self.keys.index(key), 0) + except ValueError: + return QModelIndex() + + def get_key(self, index): + """Return current key""" + return self.keys[index.row()] + + def get_value(self, index): + """Return current value""" + if index.column() == 0: + return self.keys[ index.row() ] + elif index.column() == 1: + return self.types[ index.row() ] + elif index.column() == 2: + return self.sizes[ index.row() ] + else: + return self._data[ self.keys[index.row()] ] + + def get_bgcolor(self, index): + """Background color depending on value""" + if index.column() == 0: + color = QColor(Qt.lightGray) + color.setAlphaF(.05) + elif index.column() < 3: + color = QColor(Qt.lightGray) + color.setAlphaF(.2) + else: + color = QColor(Qt.lightGray) + color.setAlphaF(.3) + return color + + def data(self, index, role=Qt.DisplayRole): + """Cell content""" + if not index.isValid(): + return to_qvariant() + value = self.get_value(index) + if index.column() == 3 and self.remote: + value = value['view'] + if index.column() == 3: + display = value_to_display(value, minmax=self.minmax) + else: + display = to_text_string(value) + if role == Qt.DisplayRole: + return to_qvariant(display) + elif role == Qt.EditRole: + return to_qvariant(value_to_display(value)) + elif role == Qt.TextAlignmentRole: + if index.column() == 3: + if len(display.splitlines()) < 3: + return to_qvariant(int(Qt.AlignLeft|Qt.AlignVCenter)) + else: + return to_qvariant(int(Qt.AlignLeft|Qt.AlignTop)) + else: + return to_qvariant(int(Qt.AlignLeft|Qt.AlignVCenter)) + elif role == Qt.BackgroundColorRole: + return to_qvariant( self.get_bgcolor(index) ) + elif role == Qt.FontRole: + return to_qvariant(get_font(font_size_delta=DEFAULT_SMALL_DELTA)) + return to_qvariant() + + def headerData(self, section, orientation, role=Qt.DisplayRole): + """Overriding method headerData""" + if role != Qt.DisplayRole: + return to_qvariant() + i_column = int(section) + if orientation == Qt.Horizontal: + headers = (self.header0, _("Type"), _("Size"), _("Value")) + return to_qvariant( headers[i_column] ) + else: + return to_qvariant() + + def flags(self, index): + """Overriding method flags""" + # This method was implemented in CollectionsModel only, but to enable + # tuple exploration (even without editing), this method was moved here + if not index.isValid(): + return Qt.ItemIsEnabled + return Qt.ItemFlags(QAbstractTableModel.flags(self, index)| + Qt.ItemIsEditable) + def reset(self): + self.beginResetModel() + self.endResetModel() + + +class CollectionsModel(ReadOnlyCollectionsModel): + """Collections Table Model""" + + def set_value(self, index, value): + """Set value""" + self._data[ self.keys[index.row()] ] = value + self.showndata[ self.keys[index.row()] ] = value + self.sizes[index.row()] = get_size(value) + self.types[index.row()] = get_human_readable_type(value) + + def get_bgcolor(self, index): + """Background color depending on value""" + value = self.get_value(index) + if index.column() < 3: + color = ReadOnlyCollectionsModel.get_bgcolor(self, index) + else: + if self.remote: + color_name = value['color'] + else: + color_name = get_color_name(value) + color = QColor(color_name) + color.setAlphaF(.2) + return color + + def setData(self, index, value, role=Qt.EditRole): + """Cell content change""" + if not index.isValid(): + return False + if index.column() < 3: + return False + value = display_to_value(value, self.get_value(index), + ignore_errors=True) + self.set_value(index, value) + self.dataChanged.emit(index, index) + return True + + +class CollectionsDelegate(QItemDelegate): + """CollectionsEditor Item Delegate""" + + def __init__(self, parent=None): + QItemDelegate.__init__(self, parent) + self._editors = {} # keep references on opened editors + + def get_value(self, index): + if index.isValid(): + return index.model().get_value(index) + + def set_value(self, index, value): + if index.isValid(): + index.model().set_value(index, value) + + def show_warning(self, index): + """ + Decide if showing a warning when the user is trying to view + a big variable associated to a Tablemodel index + + This avoids getting the variables' value to know its + size and type, using instead those already computed by + the TableModel. + + The problem is when a variable is too big, it can take a + lot of time just to get its value + """ + try: + val_size = index.model().sizes[index.row()] + val_type = index.model().types[index.row()] + except: + return False + if val_type in ['list', 'tuple', 'dict'] and int(val_size) > 1e5: + return True + else: + return False + + def createEditor(self, parent, option, index): + """Overriding method createEditor""" + if index.column() < 3: + return None + if self.show_warning(index): + answer = QMessageBox.warning(self.parent(), _("Warning"), + _("Opening this variable can be slow\n\n" + "Do you want to continue anyway?"), + QMessageBox.Yes | QMessageBox.No) + if answer == QMessageBox.No: + return None + try: + value = self.get_value(index) + except Exception as msg: + QMessageBox.critical(self.parent(), _("Error"), + _("Spyder was unable to retrieve the value of " + "this variable from the console.

    " + "The error mesage was:
    " + "%s" + ) % to_text_string(msg)) + return + key = index.model().get_key(index) + readonly = isinstance(value, tuple) or self.parent().readonly \ + or not is_known_type(value) + #---editor = CollectionsEditor + if isinstance(value, (list, tuple, dict)): + editor = CollectionsEditor() + editor.setup(value, key, icon=self.parent().windowIcon(), + readonly=readonly) + self.create_dialog(editor, dict(model=index.model(), editor=editor, + key=key, readonly=readonly)) + return None + #---editor = ArrayEditor + elif isinstance(value, (ndarray, MaskedArray)) \ + and ndarray is not FakeObject: + if value.size == 0: + return None + editor = ArrayEditor(parent) + if not editor.setup_and_check(value, title=key, readonly=readonly): + return + self.create_dialog(editor, dict(model=index.model(), editor=editor, + key=key, readonly=readonly)) + return None + #---showing image + elif isinstance(value, Image) and ndarray is not FakeObject \ + and Image is not FakeObject: + arr = array(value) + if arr.size == 0: + return None + editor = ArrayEditor(parent) + if not editor.setup_and_check(arr, title=key, readonly=readonly): + return + conv_func = lambda arr: Image.fromarray(arr, mode=value.mode) + self.create_dialog(editor, dict(model=index.model(), editor=editor, + key=key, readonly=readonly, + conv=conv_func)) + return None + #--editor = DataFrameEditor + elif isinstance(value, (DataFrame, Series)) \ + and DataFrame is not FakeObject: + editor = DataFrameEditor() + if not editor.setup_and_check(value, title=key): + return + self.create_dialog(editor, dict(model=index.model(), editor=editor, + key=key, readonly=readonly)) + return None + #---editor = QDateTimeEdit + elif isinstance(value, datetime.datetime): + editor = QDateTimeEdit(value, parent) + editor.setCalendarPopup(True) + editor.setFont(get_font(font_size_delta=DEFAULT_SMALL_DELTA)) + return editor + #---editor = QDateEdit + elif isinstance(value, datetime.date): + editor = QDateEdit(value, parent) + editor.setCalendarPopup(True) + editor.setFont(get_font(font_size_delta=DEFAULT_SMALL_DELTA)) + return editor + #---editor = TextEditor + elif is_text_string(value) and len(value)>40: + editor = TextEditor(value, key) + self.create_dialog(editor, dict(model=index.model(), editor=editor, + key=key, readonly=readonly)) + return None + #---editor = QLineEdit + elif is_editable_type(value): + editor = QLineEdit(parent) + editor.setFont(get_font(font_size_delta=DEFAULT_SMALL_DELTA)) + editor.setAlignment(Qt.AlignLeft) + # This is making Spyder crash because the QLineEdit that it's + # been modified is removed and a new one is created after + # evaluation. So the object on which this method is trying to + # act doesn't exist anymore. + # editor.returnPressed.connect(self.commitAndCloseEditor) + return editor + #---editor = CollectionsEditor for an arbitrary object + else: + editor = CollectionsEditor() + editor.setup(value, key, icon=self.parent().windowIcon(), + readonly=readonly) + self.create_dialog(editor, dict(model=index.model(), editor=editor, + key=key, readonly=readonly)) + return None + + def create_dialog(self, editor, data): + self._editors[id(editor)] = data + editor.accepted.connect( + lambda eid=id(editor): self.editor_accepted(eid)) + editor.rejected.connect( + lambda eid=id(editor): self.editor_rejected(eid)) + editor.show() + + def editor_accepted(self, editor_id): + data = self._editors[editor_id] + if not data['readonly']: + index = data['model'].get_index_from_key(data['key']) + value = data['editor'].get_value() + conv_func = data.get('conv', lambda v: v) + self.set_value(index, conv_func(value)) + self._editors.pop(editor_id) + self.free_memory() + + def editor_rejected(self, editor_id): + self._editors.pop(editor_id) + self.free_memory() + + def free_memory(self): + """Free memory after closing an editor.""" + gc.collect() + + def commitAndCloseEditor(self): + """Overriding method commitAndCloseEditor""" + editor = self.sender() + # Avoid a segfault with PyQt5. Variable value won't be changed + # but at least Spyder won't crash. It seems generated by a + # bug in sip. See + # http://comments.gmane.org/gmane.comp.python.pyqt-pykde/26544 + try: + self.commitData.emit(editor) + except AttributeError: + pass + self.closeEditor.emit(editor, QAbstractItemDelegate.NoHint) + + def setEditorData(self, editor, index): + """ + Overriding method setEditorData + Model --> Editor + """ + value = self.get_value(index) + if isinstance(editor, QLineEdit): + if is_binary_string(value): + try: + value = to_text_string(value, 'utf8') + except: + pass + if not is_text_string(value): + value = repr(value) + editor.setText(value) + elif isinstance(editor, QDateEdit): + editor.setDate(value) + elif isinstance(editor, QDateTimeEdit): + editor.setDateTime(QDateTime(value.date(), value.time())) + + def setModelData(self, editor, model, index): + """ + Overriding method setModelData + Editor --> Model + """ + if not hasattr(model, "set_value"): + # Read-only mode + return + + if isinstance(editor, QLineEdit): + value = editor.text() + try: + value = display_to_value(to_qvariant(value), + self.get_value(index), + ignore_errors=False) + except Exception as msg: + raise + QMessageBox.critical(editor, _("Edit item"), + _("Unable to assign data to item." + "

    Error message:
    %s" + ) % str(msg)) + return + elif isinstance(editor, QDateEdit): + qdate = editor.date() + value = datetime.date( qdate.year(), qdate.month(), qdate.day() ) + elif isinstance(editor, QDateTimeEdit): + qdatetime = editor.dateTime() + qdate = qdatetime.date() + qtime = qdatetime.time() + value = datetime.datetime( qdate.year(), qdate.month(), + qdate.day(), qtime.hour(), + qtime.minute(), qtime.second() ) + else: + # Should not happen... + raise RuntimeError("Unsupported editor widget") + self.set_value(index, value) + + +class BaseTableView(QTableView): + """Base collection editor table view""" + sig_option_changed = Signal(str, object) + sig_files_dropped = Signal(list) + redirect_stdio = Signal(bool) + + def __init__(self, parent): + QTableView.__init__(self, parent) + self.array_filename = None + self.menu = None + self.empty_ws_menu = None + self.paste_action = None + self.copy_action = None + self.edit_action = None + self.plot_action = None + self.hist_action = None + self.imshow_action = None + self.save_array_action = None + self.insert_action = None + self.remove_action = None + self.minmax_action = None + self.rename_action = None + self.duplicate_action = None + self.delegate = None + self.setAcceptDrops(True) + + def setup_table(self): + """Setup table""" + self.horizontalHeader().setStretchLastSection(True) + self.adjust_columns() + # Sorting columns + self.setSortingEnabled(True) + self.sortByColumn(0, Qt.AscendingOrder) + + def setup_menu(self, minmax): + """Setup context menu""" + if self.minmax_action is not None: + self.minmax_action.setChecked(minmax) + return + + resize_action = create_action(self, _("Resize rows to contents"), + triggered=self.resizeRowsToContents) + self.paste_action = create_action(self, _("Paste"), + icon=ima.icon('editpaste'), + triggered=self.paste) + self.copy_action = create_action(self, _("Copy"), + icon=ima.icon('editcopy'), + triggered=self.copy) + self.edit_action = create_action(self, _("Edit"), + icon=ima.icon('edit'), + triggered=self.edit_item) + self.plot_action = create_action(self, _("Plot"), + icon=ima.icon('plot'), + triggered=lambda: self.plot_item('plot')) + self.plot_action.setVisible(False) + self.hist_action = create_action(self, _("Histogram"), + icon=ima.icon('hist'), + triggered=lambda: self.plot_item('hist')) + self.hist_action.setVisible(False) + self.imshow_action = create_action(self, _("Show image"), + icon=ima.icon('imshow'), + triggered=self.imshow_item) + self.imshow_action.setVisible(False) + self.save_array_action = create_action(self, _("Save array"), + icon=ima.icon('filesave'), + triggered=self.save_array) + self.save_array_action.setVisible(False) + self.insert_action = create_action(self, _("Insert"), + icon=ima.icon('insert'), + triggered=self.insert_item) + self.remove_action = create_action(self, _("Remove"), + icon=ima.icon('editdelete'), + triggered=self.remove_item) + self.minmax_action = create_action(self, _("Show arrays min/max"), + toggled=self.toggle_minmax) + self.minmax_action.setChecked(minmax) + self.toggle_minmax(minmax) + self.rename_action = create_action(self, _("Rename"), + icon=ima.icon('rename'), + triggered=self.rename_item) + self.duplicate_action = create_action(self, _("Duplicate"), + icon=ima.icon('edit_add'), + triggered=self.duplicate_item) + menu = QMenu(self) + menu_actions = [self.edit_action, self.plot_action, self.hist_action, + self.imshow_action, self.save_array_action, + self.insert_action, self.remove_action, + self.copy_action, self.paste_action, + None, self.rename_action, self.duplicate_action, + None, resize_action] + if ndarray is not FakeObject: + menu_actions.append(self.minmax_action) + add_actions(menu, menu_actions) + self.empty_ws_menu = QMenu(self) + add_actions(self.empty_ws_menu, + [self.insert_action, self.paste_action, + None, resize_action]) + return menu + + #------ Remote/local API --------------------------------------------------- + def remove_values(self, keys): + """Remove values from data""" + raise NotImplementedError + + def copy_value(self, orig_key, new_key): + """Copy value""" + raise NotImplementedError + + def new_value(self, key, value): + """Create new value in data""" + raise NotImplementedError + + def is_list(self, key): + """Return True if variable is a list or a tuple""" + raise NotImplementedError + + def get_len(self, key): + """Return sequence length""" + raise NotImplementedError + + def is_array(self, key): + """Return True if variable is a numpy array""" + raise NotImplementedError + + def is_image(self, key): + """Return True if variable is a PIL.Image image""" + raise NotImplementedError + + def is_dict(self, key): + """Return True if variable is a dictionary""" + raise NotImplementedError + + def get_array_shape(self, key): + """Return array's shape""" + raise NotImplementedError + + def get_array_ndim(self, key): + """Return array's ndim""" + raise NotImplementedError + + def oedit(self, key): + """Edit item""" + raise NotImplementedError + + def plot(self, key, funcname): + """Plot item""" + raise NotImplementedError + + def imshow(self, key): + """Show item's image""" + raise NotImplementedError + + def show_image(self, key): + """Show image (item is a PIL image)""" + raise NotImplementedError + #--------------------------------------------------------------------------- + + def refresh_menu(self): + """Refresh context menu""" + index = self.currentIndex() + condition = index.isValid() + self.edit_action.setEnabled( condition ) + self.remove_action.setEnabled( condition ) + self.refresh_plot_entries(index) + + def refresh_plot_entries(self, index): + if index.isValid(): + key = self.model.get_key(index) + is_list = self.is_list(key) + is_array = self.is_array(key) and self.get_len(key) != 0 + condition_plot = (is_array and len(self.get_array_shape(key)) <= 2) + condition_hist = (is_array and self.get_array_ndim(key) == 1) + condition_imshow = condition_plot and self.get_array_ndim(key) == 2 + condition_imshow = condition_imshow or self.is_image(key) + else: + is_array = condition_plot = condition_imshow = is_list \ + = condition_hist = False + self.plot_action.setVisible(condition_plot or is_list) + self.hist_action.setVisible(condition_hist or is_list) + self.imshow_action.setVisible(condition_imshow) + self.save_array_action.setVisible(is_array) + + def adjust_columns(self): + """Resize two first columns to contents""" + for col in range(3): + self.resizeColumnToContents(col) + + def set_data(self, data): + """Set table data""" + if data is not None: + self.model.set_data(data, self.dictfilter) + self.sortByColumn(0, Qt.AscendingOrder) + + def mousePressEvent(self, event): + """Reimplement Qt method""" + if event.button() != Qt.LeftButton: + QTableView.mousePressEvent(self, event) + return + index_clicked = self.indexAt(event.pos()) + if index_clicked.isValid(): + if index_clicked == self.currentIndex() \ + and index_clicked in self.selectedIndexes(): + self.clearSelection() + else: + QTableView.mousePressEvent(self, event) + else: + self.clearSelection() + event.accept() + + def mouseDoubleClickEvent(self, event): + """Reimplement Qt method""" + index_clicked = self.indexAt(event.pos()) + if index_clicked.isValid(): + row = index_clicked.row() + # TODO: Remove hard coded "Value" column number (3 here) + index_clicked = index_clicked.child(row, 3) + self.edit(index_clicked) + else: + event.accept() + + def keyPressEvent(self, event): + """Reimplement Qt methods""" + if event.key() == Qt.Key_Delete: + self.remove_item() + elif event.key() == Qt.Key_F2: + self.rename_item() + elif event == QKeySequence.Copy: + self.copy() + elif event == QKeySequence.Paste: + self.paste() + else: + QTableView.keyPressEvent(self, event) + + def contextMenuEvent(self, event): + """Reimplement Qt method""" + if self.model.showndata: + self.refresh_menu() + self.menu.popup(event.globalPos()) + event.accept() + else: + self.empty_ws_menu.popup(event.globalPos()) + event.accept() + + def dragEnterEvent(self, event): + """Allow user to drag files""" + if mimedata2url(event.mimeData()): + event.accept() + else: + event.ignore() + + def dragMoveEvent(self, event): + """Allow user to move files""" + if mimedata2url(event.mimeData()): + event.setDropAction(Qt.CopyAction) + event.accept() + else: + event.ignore() + + def dropEvent(self, event): + """Allow user to drop supported files""" + urls = mimedata2url(event.mimeData()) + if urls: + event.setDropAction(Qt.CopyAction) + event.accept() + self.sig_files_dropped.emit(urls) + else: + event.ignore() + + @Slot(bool) + def toggle_minmax(self, state): + """Toggle min/max display for numpy arrays""" + self.sig_option_changed.emit('minmax', state) + self.model.minmax = state + + @Slot() + def edit_item(self): + """Edit item""" + index = self.currentIndex() + if not index.isValid(): + return + # TODO: Remove hard coded "Value" column number (3 here) + self.edit(index.child(index.row(), 3)) + + @Slot() + def remove_item(self): + """Remove item""" + indexes = self.selectedIndexes() + if not indexes: + return + for index in indexes: + if not index.isValid(): + return + one = _("Do you want to remove the selected item?") + more = _("Do you want to remove all selected items?") + answer = QMessageBox.question(self, _( "Remove"), + one if len(indexes) == 1 else more, + QMessageBox.Yes | QMessageBox.No) + if answer == QMessageBox.Yes: + idx_rows = unsorted_unique([idx.row() for idx in indexes]) + keys = [ self.model.keys[idx_row] for idx_row in idx_rows ] + self.remove_values(keys) + + def copy_item(self, erase_original=False): + """Copy item""" + indexes = self.selectedIndexes() + if not indexes: + return + idx_rows = unsorted_unique([idx.row() for idx in indexes]) + if len(idx_rows) > 1 or not indexes[0].isValid(): + return + orig_key = self.model.keys[idx_rows[0]] + if erase_original: + title = _('Rename') + field_text = _('New variable name:') + else: + title = _('Duplicate') + field_text = _('Variable name:') + new_key, valid = QInputDialog.getText(self, title, field_text, + QLineEdit.Normal, orig_key) + if valid and to_text_string(new_key): + new_key = try_to_eval(to_text_string(new_key)) + if new_key == orig_key: + return + self.copy_value(orig_key, new_key) + if erase_original: + self.remove_values([orig_key]) + + @Slot() + def duplicate_item(self): + """Duplicate item""" + self.copy_item() + + @Slot() + def rename_item(self): + """Rename item""" + self.copy_item(True) + + @Slot() + def insert_item(self): + """Insert item""" + index = self.currentIndex() + if not index.isValid(): + row = self.model.rowCount() + else: + row = index.row() + data = self.model.get_data() + if isinstance(data, list): + key = row + data.insert(row, '') + elif isinstance(data, dict): + key, valid = QInputDialog.getText(self, _( 'Insert'), _( 'Key:'), + QLineEdit.Normal) + if valid and to_text_string(key): + key = try_to_eval(to_text_string(key)) + else: + return + else: + return + value, valid = QInputDialog.getText(self, _('Insert'), _('Value:'), + QLineEdit.Normal) + if valid and to_text_string(value): + self.new_value(key, try_to_eval(to_text_string(value))) + + def __prepare_plot(self): + try: + import guiqwt.pyplot #analysis:ignore + return True + except (ImportError, AssertionError): + try: + if 'matplotlib' not in sys.modules: + import matplotlib + matplotlib.use("Qt4Agg") + return True + except ImportError: + QMessageBox.warning(self, _("Import error"), + _("Please install matplotlib" + " or guiqwt.")) + + def plot_item(self, funcname): + """Plot item""" + index = self.currentIndex() + if self.__prepare_plot(): + key = self.model.get_key(index) + try: + self.plot(key, funcname) + except (ValueError, TypeError) as error: + QMessageBox.critical(self, _( "Plot"), + _("Unable to plot data." + "

    Error message:
    %s" + ) % str(error)) + + @Slot() + def imshow_item(self): + """Imshow item""" + index = self.currentIndex() + if self.__prepare_plot(): + key = self.model.get_key(index) + try: + if self.is_image(key): + self.show_image(key) + else: + self.imshow(key) + except (ValueError, TypeError) as error: + QMessageBox.critical(self, _( "Plot"), + _("Unable to show image." + "

    Error message:
    %s" + ) % str(error)) + + @Slot() + def save_array(self): + """Save array""" + title = _( "Save array") + if self.array_filename is None: + self.array_filename = getcwd() + self.redirect_stdio.emit(False) + filename, _selfilter = getsavefilename(self, title, + self.array_filename, + _("NumPy arrays")+" (*.npy)") + self.redirect_stdio.emit(True) + if filename: + self.array_filename = filename + data = self.delegate.get_value( self.currentIndex() ) + try: + import numpy as np + np.save(self.array_filename, data) + except Exception as error: + QMessageBox.critical(self, title, + _("Unable to save array" + "

    Error message:
    %s" + ) % str(error)) + + @Slot() + def copy(self): + """Copy text to clipboard""" + clipboard = QApplication.clipboard() + clipl = [] + for idx in self.selectedIndexes(): + if not idx.isValid(): + continue + obj = self.delegate.get_value(idx) + # Check if we are trying to copy a numpy array, and if so make sure + # to copy the whole thing in a tab separated format + if isinstance(obj, (ndarray, MaskedArray)) \ + and ndarray is not FakeObject: + if PY3: + output = io.BytesIO() + else: + output = io.StringIO() + try: + np_savetxt(output, obj, delimiter='\t') + except: + QMessageBox.warning(self, _("Warning"), + _("It was not possible to copy " + "this array")) + return + obj = output.getvalue().decode('utf-8') + output.close() + elif isinstance(obj, (DataFrame, Series)) \ + and DataFrame is not FakeObject: + output = io.StringIO() + obj.to_csv(output, sep='\t', index=True, header=True) + if PY3: + obj = output.getvalue() + else: + obj = output.getvalue().decode('utf-8') + output.close() + elif is_binary_string(obj): + obj = to_text_string(obj, 'utf8') + else: + obj = to_text_string(obj) + clipl.append(obj) + clipboard.setText('\n'.join(clipl)) + + def import_from_string(self, text, title=None): + """Import data from string""" + data = self.model.get_data() + editor = ImportWizard(self, text, title=title, + contents_title=_("Clipboard contents"), + varname=fix_reference_name("data", + blacklist=list(data.keys()))) + if editor.exec_(): + var_name, clip_data = editor.get_data() + self.new_value(var_name, clip_data) + + @Slot() + def paste(self): + """Import text/data/code from clipboard""" + clipboard = QApplication.clipboard() + cliptext = '' + if clipboard.mimeData().hasText(): + cliptext = to_text_string(clipboard.text()) + if cliptext.strip(): + self.import_from_string(cliptext, title=_("Import from clipboard")) + else: + QMessageBox.warning(self, _( "Empty clipboard"), + _("Nothing to be imported from clipboard.")) + + +class CollectionsEditorTableView(BaseTableView): + """CollectionsEditor table view""" + def __init__(self, parent, data, readonly=False, title="", + names=False, minmax=False): + BaseTableView.__init__(self, parent) + self.dictfilter = None + self.readonly = readonly or isinstance(data, tuple) + CollectionsModelClass = ReadOnlyCollectionsModel if self.readonly \ + else CollectionsModel + self.model = CollectionsModelClass(self, data, title, names=names, + minmax=minmax) + self.setModel(self.model) + self.delegate = CollectionsDelegate(self) + self.setItemDelegate(self.delegate) + + self.setup_table() + self.menu = self.setup_menu(minmax) + + #------ Remote/local API --------------------------------------------------- + def remove_values(self, keys): + """Remove values from data""" + data = self.model.get_data() + for key in sorted(keys, reverse=True): + data.pop(key) + self.set_data(data) + + def copy_value(self, orig_key, new_key): + """Copy value""" + data = self.model.get_data() + data[new_key] = data[orig_key] + self.set_data(data) + + def new_value(self, key, value): + """Create new value in data""" + data = self.model.get_data() + data[key] = value + self.set_data(data) + + def is_list(self, key): + """Return True if variable is a list or a tuple""" + data = self.model.get_data() + return isinstance(data[key], (tuple, list)) + + def get_len(self, key): + """Return sequence length""" + data = self.model.get_data() + return len(data[key]) + + def is_array(self, key): + """Return True if variable is a numpy array""" + data = self.model.get_data() + return isinstance(data[key], (ndarray, MaskedArray)) + + def is_image(self, key): + """Return True if variable is a PIL.Image image""" + data = self.model.get_data() + return isinstance(data[key], Image) + + def is_dict(self, key): + """Return True if variable is a dictionary""" + data = self.model.get_data() + return isinstance(data[key], dict) + + def get_array_shape(self, key): + """Return array's shape""" + data = self.model.get_data() + return data[key].shape + + def get_array_ndim(self, key): + """Return array's ndim""" + data = self.model.get_data() + return data[key].ndim + + def oedit(self, key): + """Edit item""" + data = self.model.get_data() + from spyder.widgets.variableexplorer.objecteditor import oedit + oedit(data[key]) + + def plot(self, key, funcname): + """Plot item""" + data = self.model.get_data() + import spyder.pyplot as plt + plt.figure() + getattr(plt, funcname)(data[key]) + plt.show() + + def imshow(self, key): + """Show item's image""" + data = self.model.get_data() + import spyder.pyplot as plt + plt.figure() + plt.imshow(data[key]) + plt.show() + + def show_image(self, key): + """Show image (item is a PIL image)""" + data = self.model.get_data() + data[key].show() + #--------------------------------------------------------------------------- + + def refresh_menu(self): + """Refresh context menu""" + data = self.model.get_data() + index = self.currentIndex() + condition = (not isinstance(data, tuple)) and index.isValid() \ + and not self.readonly + self.edit_action.setEnabled( condition ) + self.remove_action.setEnabled( condition ) + self.insert_action.setEnabled( not self.readonly ) + self.refresh_plot_entries(index) + + def set_filter(self, dictfilter=None): + """Set table dict filter""" + self.dictfilter = dictfilter + + +class CollectionsEditorWidget(QWidget): + """Dictionary Editor Widget""" + def __init__(self, parent, data, readonly=False, title="", remote=False): + QWidget.__init__(self, parent) + if remote: + self.editor = RemoteCollectionsEditorTableView(self, data, readonly) + else: + self.editor = CollectionsEditorTableView(self, data, readonly, + title) + layout = QVBoxLayout() + layout.addWidget(self.editor) + self.setLayout(layout) + + def set_data(self, data): + """Set DictEditor data""" + self.editor.set_data(data) + + def get_title(self): + """Get model title""" + return self.editor.model.title + + +class CollectionsEditor(QDialog): + """Collections Editor Dialog""" + def __init__(self, parent=None): + QDialog.__init__(self, parent) + + # Destroying the C++ object right after closing the dialog box, + # otherwise it may be garbage-collected in another QThread + # (e.g. the editor's analysis thread in Spyder), thus leading to + # a segmentation fault on UNIX or an application crash on Windows + self.setAttribute(Qt.WA_DeleteOnClose) + + self.data_copy = None + self.widget = None + + def setup(self, data, title='', readonly=False, width=500, remote=False, + icon=None, parent=None): + if isinstance(data, dict): + # dictionnary + self.data_copy = data.copy() + datalen = len(data) + elif isinstance(data, (tuple, list)): + # list, tuple + self.data_copy = data[:] + datalen = len(data) + else: + # unknown object + import copy + self.data_copy = copy.deepcopy(data) + datalen = len(dir(data)) + self.widget = CollectionsEditorWidget(self, self.data_copy, title=title, + readonly=readonly, remote=remote) + + layout = QVBoxLayout() + layout.addWidget(self.widget) + self.setLayout(layout) + + # Buttons configuration + buttons = QDialogButtonBox.Ok + if not readonly: + buttons = buttons | QDialogButtonBox.Cancel + bbox = QDialogButtonBox(buttons) + bbox.accepted.connect(self.accept) + if not readonly: + bbox.rejected.connect(self.reject) + layout.addWidget(bbox) + + constant = 121 + row_height = 30 + error_margin = 20 + height = constant + row_height*min([15, datalen]) + error_margin + self.resize(width, height) + + self.setWindowTitle(self.widget.get_title()) + if icon is None: + self.setWindowIcon(ima.icon('dictedit')) + # Make the dialog act as a window + self.setWindowFlags(Qt.Window) + + def get_value(self): + """Return modified copy of dictionary or list""" + # It is import to avoid accessing Qt C++ object as it has probably + # already been destroyed, due to the Qt.WA_DeleteOnClose attribute + return self.data_copy + + +class DictEditor(CollectionsEditor): + def __init__(self, parent=None): + warnings.warn("`DictEditor` has been renamed to `CollectionsEditor` in " + "Spyder 3. Please use `CollectionsEditor` instead", + RuntimeWarning) + CollectionsEditor.__init__(self, parent) + + +#----Remote versions of CollectionsDelegate and CollectionsEditorTableView +class RemoteCollectionsDelegate(CollectionsDelegate): + """CollectionsEditor Item Delegate""" + def __init__(self, parent=None, get_value_func=None, set_value_func=None): + CollectionsDelegate.__init__(self, parent) + self.get_value_func = get_value_func + self.set_value_func = set_value_func + + def get_value(self, index): + if index.isValid(): + name = index.model().keys[index.row()] + return self.get_value_func(name) + + def set_value(self, index, value): + if index.isValid(): + name = index.model().keys[index.row()] + self.set_value_func(name, value) + + +class RemoteCollectionsEditorTableView(BaseTableView): + """DictEditor table view""" + def __init__(self, parent, data, minmax=False, + get_value_func=None, set_value_func=None, + new_value_func=None, remove_values_func=None, + copy_value_func=None, is_list_func=None, get_len_func=None, + is_array_func=None, is_image_func=None, is_dict_func=None, + get_array_shape_func=None, get_array_ndim_func=None, + oedit_func=None, plot_func=None, imshow_func=None, + is_data_frame_func=None, is_series_func=None, + show_image_func=None, remote_editing=False): + BaseTableView.__init__(self, parent) + + self.remote_editing_enabled = None + + self.remove_values = remove_values_func + self.copy_value = copy_value_func + self.new_value = new_value_func + + self.is_data_frame = is_data_frame_func + self.is_series = is_series_func + self.is_list = is_list_func + self.get_len = get_len_func + self.is_array = is_array_func + self.is_image = is_image_func + self.is_dict = is_dict_func + self.get_array_shape = get_array_shape_func + self.get_array_ndim = get_array_ndim_func + self.oedit = oedit_func + self.plot = plot_func + self.imshow = imshow_func + self.show_image = show_image_func + + self.dictfilter = None + self.model = None + self.delegate = None + self.readonly = False + self.model = CollectionsModel(self, data, names=True, + minmax=minmax, + remote=True) + self.setModel(self.model) + self.delegate = RemoteCollectionsDelegate(self, get_value_func, + set_value_func) + self.setItemDelegate(self.delegate) + + self.setup_table() + self.menu = self.setup_menu(minmax) + + def setup_menu(self, minmax): + """Setup context menu""" + menu = BaseTableView.setup_menu(self, minmax) + return menu + + def oedit_possible(self, key): + if (self.is_list(key) or self.is_dict(key) + or self.is_array(key) or self.is_image(key) + or self.is_data_frame(key) or self.is_series(key)): + # If this is a remote dict editor, the following avoid + # transfering large amount of data through the socket + return True + + def edit_item(self): + """ + Reimplement BaseTableView's method to edit item + + Some supported data types are directly edited in the remote process, + thus avoiding to transfer large amount of data through the socket from + the remote process to Spyder + """ + if self.remote_editing_enabled: + index = self.currentIndex() + if not index.isValid(): + return + key = self.model.get_key(index) + if self.oedit_possible(key): + # If this is a remote dict editor, the following avoid + # transfering large amount of data through the socket + self.oedit(key) + else: + BaseTableView.edit_item(self) + else: + BaseTableView.edit_item(self) + + +#============================================================================== +# Tests +#============================================================================== +def get_test_data(): + """Create test data""" + import numpy as np + from spyder.pil_patch import Image + image = Image.fromarray(np.random.random_integers(255, size=(100, 100)), + mode='P') + testdict = {'d': 1, 'a': np.random.rand(10, 10), 'b': [1, 2]} + testdate = datetime.date(1945, 5, 8) + class Foobar(object): + def __init__(self): + self.text = "toto" + self.testdict = testdict + self.testdate = testdate + foobar = Foobar() + return {'object': foobar, + 'str': 'kjkj kj k j j kj k jkj', + 'unicode': to_text_string('éù', 'utf-8'), + 'list': [1, 3, [sorted, 5, 6], 'kjkj', None], + 'tuple': ([1, testdate, testdict], 'kjkj', None), + 'dict': testdict, + 'float': 1.2233, + 'int': 223, + 'bool': True, + 'array': np.random.rand(10, 10), + 'masked_array': np.ma.array([[1, 0], [1, 0]], + mask=[[True, False], [False, False]]), + '1D-array': np.linspace(-10, 10), + 'empty_array': np.array([]), + 'image': image, + 'date': testdate, + 'datetime': datetime.datetime(1945, 5, 8), + 'complex': 2+1j, + 'complex64': np.complex64(2+1j), + 'int8_scalar': np.int8(8), + 'int16_scalar': np.int16(16), + 'int32_scalar': np.int32(32), + 'bool_scalar': np.bool(8), + 'unsupported1': np.arccos, + 'unsupported2': np.cast, + # Test for Issue #3518 + 'big_struct_array': np.zeros(1000, dtype=[('ID', 'f8'), + ('param1', 'f8', 5000)]), + } + + +def editor_test(): + """Collections editor test""" + from spyder.utils.qthelpers import qapplication + + app = qapplication() #analysis:ignore + dialog = CollectionsEditor() + dialog.setup(get_test_data()) + dialog.show() + app.exec_() + + +def remote_editor_test(): + """Remote collections editor test""" + from spyder.utils.qthelpers import qapplication + app = qapplication() + + from spyder.plugins.variableexplorer import VariableExplorer + from spyder.widgets.variableexplorer.utils import make_remote_view + + remote = make_remote_view(get_test_data(), VariableExplorer.get_settings()) + dialog = CollectionsEditor() + dialog.setup(remote, remote=True) + dialog.show() + app.exec_() + + +if __name__ == "__main__": + editor_test() + remote_editor_test() diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/widgets/variableexplorer/dataframeeditor.py spyder-3.0.2+dfsg1/spyder/widgets/variableexplorer/dataframeeditor.py --- spyder-2.3.8+dfsg1/spyder/widgets/variableexplorer/dataframeeditor.py 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/widgets/variableexplorer/dataframeeditor.py 2016-11-08 02:15:20.000000000 +0100 @@ -0,0 +1,669 @@ +# -*- coding: utf-8 -*- +# +# Copyright © Spyder Project Contributors +# Licensed under the terms of the New BSD License +# +# DataFrameModel is based on the class ArrayModel from array editor +# and the class DataFrameModel from the pandas project. +# Present in pandas.sandbox.qtpandas in v0.13.1 +# Copyright (c) 2011-2012, Lambda Foundry, Inc. +# and PyData Development Team All rights reserved + +""" +Pandas DataFrame Editor Dialog +""" + +# Third party imports +from pandas import DataFrame, Series +from qtpy import API +from qtpy.compat import from_qvariant, to_qvariant +from qtpy.QtCore import QAbstractTableModel, QModelIndex, Qt, Slot +from qtpy.QtGui import QColor, QCursor, QKeySequence +from qtpy.QtWidgets import (QApplication, QCheckBox, QDialogButtonBox, QDialog, + QGridLayout, QHBoxLayout, QInputDialog, QLineEdit, + QMenu, QMessageBox, QPushButton, QTableView) +import numpy as np + +# Local imports +from spyder.config.base import _ +from spyder.config.fonts import DEFAULT_SMALL_DELTA +from spyder.config.gui import get_font, fixed_shortcut +from spyder.py3compat import io, is_text_string, PY2, to_text_string +from spyder.utils import encoding +from spyder.utils import icon_manager as ima +from spyder.utils.qthelpers import (add_actions, create_action, + keybinding, qapplication) +from spyder.widgets.variableexplorer.arrayeditor import get_idx_rect + +# Supported Numbers and complex numbers +REAL_NUMBER_TYPES = (float, int, np.int64, np.int32) +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'] + +# Limit at which dataframe is considered so large that it is loaded on demand +LARGE_SIZE = 5e5 +LARGE_NROWS = 1e5 +LARGE_COLS = 60 + +# Background colours +BACKGROUND_NUMBER_MINHUE = 0.66 # hue for largest number +BACKGROUND_NUMBER_HUERANGE = 0.33 # (hue for smallest) minus (hue for largest) +BACKGROUND_NUMBER_SATURATION = 0.7 +BACKGROUND_NUMBER_VALUE = 1.0 +BACKGROUND_NUMBER_ALPHA = 0.6 +BACKGROUND_NONNUMBER_COLOR = Qt.lightGray +BACKGROUND_INDEX_ALPHA = 0.8 +BACKGROUND_STRING_ALPHA = 0.05 +BACKGROUND_MISC_ALPHA = 0.3 + + +def bool_false_check(value): + """ + Used to convert bool intrance to false since any string in bool('') + will return True + """ + if value.lower() in _bool_false: + value = '' + return value + + +def global_max(col_vals, index): + """Returns the global maximum and minimum""" + col_vals_without_None = [x for x in col_vals if x is not None] + max_col, min_col = zip(*col_vals_without_None) + return max(max_col), min(min_col) + + +class DataFrameModel(QAbstractTableModel): + """ DataFrame Table Model""" + + ROWS_TO_LOAD = 500 + COLS_TO_LOAD = 40 + + def __init__(self, dataFrame, format="%.3g", parent=None): + QAbstractTableModel.__init__(self) + self.dialog = parent + self.df = dataFrame + self.df_index = dataFrame.index.tolist() + self.df_header = dataFrame.columns.tolist() + self._format = format + self.complex_intran = None + + self.total_rows = self.df.shape[0] + self.total_cols = self.df.shape[1] + size = self.total_rows * self.total_cols + + self.max_min_col = None + if size < LARGE_SIZE: + self.max_min_col_update() + self.colum_avg_enabled = True + self.bgcolor_enabled = True + self.colum_avg(1) + else: + self.colum_avg_enabled = False + self.bgcolor_enabled = False + self.colum_avg(0) + + # Use paging when the total size, number of rows or number of + # columns is too large + if size > LARGE_SIZE: + self.rows_loaded = self.ROWS_TO_LOAD + self.cols_loaded = self.COLS_TO_LOAD + else: + if self.total_rows > LARGE_NROWS: + self.rows_loaded = self.ROWS_TO_LOAD + else: + self.rows_loaded = self.total_rows + if self.total_cols > LARGE_COLS: + self.cols_loaded = self.COLS_TO_LOAD + else: + self.cols_loaded = self.total_cols + + def max_min_col_update(self): + """ + Determines the maximum and minimum number in each column. + + The result is a list whose k-th entry is [vmax, vmin], where vmax and + vmin denote the maximum and minimum of the k-th column (ignoring NaN). + This list is stored in self.max_min_col. + + If the k-th column has a non-numerical dtype, then the k-th entry + is set to None. If the dtype is complex, then compute the maximum and + minimum of the absolute values. If vmax equals vmin, then vmin is + decreased by one. + """ + if self.df.shape[0] == 0: # If no rows to compute max/min then return + return + self.max_min_col = [] + for dummy, col in self.df.iteritems(): + if col.dtype in REAL_NUMBER_TYPES + COMPLEX_NUMBER_TYPES: + if col.dtype in REAL_NUMBER_TYPES: + vmax = col.max(skipna=True) + vmin = col.min(skipna=True) + else: + vmax = col.abs().max(skipna=True) + vmin = col.abs().min(skipna=True) + if vmax != vmin: + max_min = [vmax, vmin] + else: + max_min = [vmax, vmin - 1] + else: + max_min = None + self.max_min_col.append(max_min) + + def get_format(self): + """Return current format""" + # Avoid accessing the private attribute _format from outside + return self._format + + def set_format(self, format): + """Change display format""" + self._format = format + self.reset() + + def bgcolor(self, state): + """Toggle backgroundcolor""" + self.bgcolor_enabled = state > 0 + self.reset() + + def colum_avg(self, state): + """Toggle backgroundcolor""" + self.colum_avg_enabled = state > 0 + if self.colum_avg_enabled: + self.return_max = lambda col_vals, index: col_vals[index] + else: + self.return_max = global_max + self.reset() + + def headerData(self, section, orientation, role=Qt.DisplayRole): + """Set header data""" + if role != Qt.DisplayRole: + return to_qvariant() + + if orientation == Qt.Horizontal: + if section == 0: + return 'Index' + elif section == 1 and PY2: + # Get rid of possible BOM utf-8 data present at the + # beginning of a file, which gets attached to the first + # column header when headers are present in the first + # row. + # Fixes Issue 2514 + try: + header = to_text_string(self.df_header[0], + encoding='utf-8-sig') + except: + header = to_text_string(self.df_header[0]) + return to_qvariant(header) + else: + return to_qvariant(to_text_string(self.df_header[section-1])) + else: + return to_qvariant() + + def get_bgcolor(self, index): + """Background color depending on value""" + column = index.column() + if column == 0: + color = QColor(BACKGROUND_NONNUMBER_COLOR) + color.setAlphaF(BACKGROUND_INDEX_ALPHA) + return color + if not self.bgcolor_enabled: + return + value = self.get_value(index.row(), column-1) + if self.max_min_col[column - 1] is None: + color = QColor(BACKGROUND_NONNUMBER_COLOR) + if is_text_string(value): + color.setAlphaF(BACKGROUND_STRING_ALPHA) + else: + color.setAlphaF(BACKGROUND_MISC_ALPHA) + else: + if isinstance(value, COMPLEX_NUMBER_TYPES): + color_func = abs + else: + color_func = float + vmax, vmin = self.return_max(self.max_min_col, column-1) + hue = (BACKGROUND_NUMBER_MINHUE + BACKGROUND_NUMBER_HUERANGE * + (vmax - color_func(value)) / (vmax - vmin)) + hue = float(abs(hue)) + if hue > 1: + hue = 1 + color = QColor.fromHsvF(hue, BACKGROUND_NUMBER_SATURATION, + BACKGROUND_NUMBER_VALUE, BACKGROUND_NUMBER_ALPHA) + return color + + def get_value(self, row, column): + """Returns the value of the DataFrame""" + # To increase the performance iat is used but that requires error + # handling, so fallback uses iloc + try: + value = self.df.iat[row, column] + except: + value = self.df.iloc[row, column] + return value + + def update_df_index(self): + """"Update the DataFrame index""" + self.df_index = self.df.index.tolist() + + def data(self, index, role=Qt.DisplayRole): + """Cell content""" + if not index.isValid(): + return to_qvariant() + if role == Qt.DisplayRole or role == Qt.EditRole: + column = index.column() + row = index.row() + if column == 0: + return to_qvariant(to_text_string(self.df_index[row])) + else: + value = self.get_value(row, column-1) + if isinstance(value, float): + return to_qvariant(self._format % value) + else: + try: + return to_qvariant(to_text_string(value)) + except UnicodeDecodeError: + return to_qvariant(encoding.to_unicode(value)) + elif role == Qt.BackgroundColorRole: + return to_qvariant(self.get_bgcolor(index)) + elif role == Qt.FontRole: + return to_qvariant(get_font(font_size_delta=DEFAULT_SMALL_DELTA)) + return to_qvariant() + + def sort(self, column, order=Qt.AscendingOrder): + """Overriding sort method""" + if self.complex_intran is not None: + if self.complex_intran.any(axis=0).iloc[column-1]: + QMessageBox.critical(self.dialog, "Error", + "TypeError error: no ordering " + "relation is defined for complex numbers") + return False + try: + ascending = order == Qt.AscendingOrder + if column > 0: + try: + self.df.sort_values(by=self.df.columns[column-1], + ascending=ascending, inplace=True, + kind='mergesort') + except AttributeError: + # for pandas version < 0.17 + self.df.sort(columns=self.df.columns[column-1], + ascending=ascending, inplace=True, + kind='mergesort') + self.update_df_index() + else: + self.df.sort_index(inplace=True, ascending=ascending) + self.update_df_index() + except TypeError as e: + QMessageBox.critical(self.dialog, "Error", + "TypeError error: %s" % str(e)) + return False + + self.reset() + return True + + def flags(self, index): + """Set flags""" + if index.column() == 0: + return Qt.ItemIsEnabled | Qt.ItemIsSelectable + return Qt.ItemFlags(QAbstractTableModel.flags(self, index) | + Qt.ItemIsEditable) + + def setData(self, index, value, role=Qt.EditRole, change_type=None): + """Cell content change""" + column = index.column() + row = index.row() + + if change_type is not None: + try: + value = self.data(index, role=Qt.DisplayRole) + val = from_qvariant(value, str) + if change_type is bool: + val = bool_false_check(val) + self.df.iloc[row, column - 1] = change_type(val) + except ValueError: + self.df.iloc[row, column - 1] = change_type('0') + else: + val = from_qvariant(value, str) + current_value = self.get_value(row, column-1) + if isinstance(current_value, bool): + val = bool_false_check(val) + supported_types = (bool,) + REAL_NUMBER_TYPES + COMPLEX_NUMBER_TYPES + if (isinstance(current_value, supported_types) or + is_text_string(current_value)): + try: + self.df.iloc[row, column-1] = current_value.__class__(val) + except ValueError as e: + QMessageBox.critical(self.dialog, "Error", + "Value error: %s" % str(e)) + return False + else: + QMessageBox.critical(self.dialog, "Error", + "The type of the cell is not a supported " + "type") + return False + self.max_min_col_update() + return True + + def get_data(self): + """Return data""" + return self.df + + def rowCount(self, index=QModelIndex()): + """DataFrame row number""" + if self.total_rows <= self.rows_loaded: + return self.total_rows + else: + return self.rows_loaded + + def can_fetch_more(self, rows=False, columns=False): + if rows: + if self.total_rows > self.rows_loaded: + return True + else: + return False + if columns: + if self.total_cols > self.cols_loaded: + return True + else: + return False + + def fetch_more(self, rows=False, columns=False): + if self.can_fetch_more(rows=rows): + reminder = self.total_rows - self.rows_loaded + items_to_fetch = min(reminder, self.ROWS_TO_LOAD) + self.beginInsertRows(QModelIndex(), self.rows_loaded, + self.rows_loaded + items_to_fetch - 1) + self.rows_loaded += items_to_fetch + self.endInsertRows() + if self.can_fetch_more(columns=columns): + reminder = self.total_cols - self.cols_loaded + items_to_fetch = min(reminder, self.COLS_TO_LOAD) + self.beginInsertColumns(QModelIndex(), self.cols_loaded, + self.cols_loaded + items_to_fetch - 1) + self.cols_loaded += items_to_fetch + self.endInsertColumns() + + def columnCount(self, index=QModelIndex()): + """DataFrame column number""" + # This is done to implement series + if len(self.df.shape) == 1: + return 2 + elif self.total_cols <= self.cols_loaded: + return self.total_cols + 1 + else: + return self.cols_loaded + 1 + + def reset(self): + self.beginResetModel() + self.endResetModel() + + +class DataFrameView(QTableView): + """Data Frame view class""" + def __init__(self, parent, model): + QTableView.__init__(self, parent) + self.setModel(model) + + self.sort_old = [None] + self.header_class = self.horizontalHeader() + self.header_class.sectionClicked.connect(self.sortByColumn) + self.menu = self.setup_menu() + fixed_shortcut(QKeySequence.Copy, self, self.copy) + self.horizontalScrollBar().valueChanged.connect( + lambda val: self.load_more_data(val, columns=True)) + self.verticalScrollBar().valueChanged.connect( + lambda val: self.load_more_data(val, rows=True)) + + def load_more_data(self, value, rows=False, columns=False): + if rows and value == self.verticalScrollBar().maximum(): + self.model().fetch_more(rows=rows) + if columns and value == self.horizontalScrollBar().maximum(): + self.model().fetch_more(columns=columns) + + def sortByColumn(self, index): + """ Implement a Column sort """ + if self.sort_old == [None]: + self.header_class.setSortIndicatorShown(True) + sort_order = self.header_class.sortIndicatorOrder() + if not self.model().sort(index, sort_order): + if len(self.sort_old) != 2: + self.header_class.setSortIndicatorShown(False) + else: + self.header_class.setSortIndicator(self.sort_old[0], + self.sort_old[1]) + return + self.sort_old = [index, self.header_class.sortIndicatorOrder()] + + def contextMenuEvent(self, event): + """Reimplement Qt method""" + self.menu.popup(event.globalPos()) + event.accept() + + def setup_menu(self): + """Setup context menu""" + copy_action = create_action(self, _('Copy'), + shortcut=keybinding('Copy'), + icon=ima.icon('editcopy'), + triggered=self.copy, + context=Qt.WidgetShortcut) + functions = ((_("To bool"), bool), (_("To complex"), complex), + (_("To int"), int), (_("To float"), float), + (_("To str"), to_text_string)) + types_in_menu = [copy_action] + for name, func in functions: + # QAction.triggered works differently for PySide and PyQt + if not API == 'pyside': + slot = lambda _checked, func=func: self.change_type(func) + else: + slot = lambda func=func: self.change_type(func) + types_in_menu += [create_action(self, name, + triggered=slot, + context=Qt.WidgetShortcut)] + menu = QMenu(self) + add_actions(menu, types_in_menu) + return menu + + def change_type(self, func): + """A function that changes types of cells""" + model = self.model() + index_list = self.selectedIndexes() + [model.setData(i, '', change_type=func) for i in index_list] + + @Slot() + def copy(self): + """Copy text to clipboard""" + if not self.selectedIndexes(): + return + (row_min, row_max, + col_min, col_max) = get_idx_rect(self.selectedIndexes()) + index = header = False + if col_min == 0: + col_min = 1 + index = True + df = self.model().df + if col_max == 0: # To copy indices + contents = '\n'.join(map(str, df.index.tolist()[slice(row_min, + row_max+1)])) + else: # To copy DataFrame + if (col_min == 0 or col_min == 1) and (df.shape[1] == col_max): + header = True + obj = df.iloc[slice(row_min, row_max+1), slice(col_min-1, col_max)] + output = io.StringIO() + obj.to_csv(output, sep='\t', index=index, header=header) + if not PY2: + contents = output.getvalue() + else: + contents = output.getvalue().decode('utf-8') + output.close() + clipboard = QApplication.clipboard() + clipboard.setText(contents) + + +class DataFrameEditor(QDialog): + """ Data Frame Editor Dialog """ + def __init__(self, parent=None): + QDialog.__init__(self, parent) + # Destroying the C++ object right after closing the dialog box, + # otherwise it may be garbage-collected in another QThread + # (e.g. the editor's analysis thread in Spyder), thus leading to + # a segmentation fault on UNIX or an application crash on Windows + self.setAttribute(Qt.WA_DeleteOnClose) + self.is_series = False + self.layout = None + + def setup_and_check(self, data, title=''): + """ + Setup DataFrameEditor: + return False if data is not supported, True otherwise + """ + self.layout = QGridLayout() + self.setLayout(self.layout) + self.setWindowIcon(ima.icon('arredit')) + if title: + title = to_text_string(title) + " - %s" % data.__class__.__name__ + else: + title = _("%s editor") % data.__class__.__name__ + if isinstance(data, Series): + self.is_series = True + data = data.to_frame() + + self.setWindowTitle(title) + self.resize(600, 500) + + self.dataModel = DataFrameModel(data, parent=self) + self.dataTable = DataFrameView(self, self.dataModel) + + self.layout.addWidget(self.dataTable) + self.setLayout(self.layout) + self.setMinimumSize(400, 300) + # Make the dialog act as a window + self.setWindowFlags(Qt.Window) + btn_layout = QHBoxLayout() + + btn = QPushButton(_("Format")) + # disable format button for int type + btn_layout.addWidget(btn) + btn.clicked.connect(self.change_format) + btn = QPushButton(_('Resize')) + btn_layout.addWidget(btn) + btn.clicked.connect(self.resize_to_contents) + + bgcolor = QCheckBox(_('Background color')) + bgcolor.setChecked(self.dataModel.bgcolor_enabled) + bgcolor.setEnabled(self.dataModel.bgcolor_enabled) + bgcolor.stateChanged.connect(self.change_bgcolor_enable) + btn_layout.addWidget(bgcolor) + + self.bgcolor_global = QCheckBox(_('Column min/max')) + self.bgcolor_global.setChecked(self.dataModel.colum_avg_enabled) + self.bgcolor_global.setEnabled(not self.is_series and + self.dataModel.bgcolor_enabled) + self.bgcolor_global.stateChanged.connect(self.dataModel.colum_avg) + btn_layout.addWidget(self.bgcolor_global) + + btn_layout.addStretch() + bbox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) + bbox.accepted.connect(self.accept) + bbox.rejected.connect(self.reject) + btn_layout.addWidget(bbox) + + self.layout.addLayout(btn_layout, 2, 0) + + return True + + def change_bgcolor_enable(self, state): + """ + This is implementet so column min/max is only active when bgcolor is + """ + self.dataModel.bgcolor(state) + self.bgcolor_global.setEnabled(not self.is_series and state > 0) + + def change_format(self): + """Change display format""" + format, valid = QInputDialog.getText(self, _('Format'), + _("Float formatting"), + QLineEdit.Normal, + self.dataModel.get_format()) + if valid: + format = str(format) + try: + format % 1.1 + except: + QMessageBox.critical(self, _("Error"), + _("Format (%s) is incorrect") % format) + return + self.dataModel.set_format(format) + + def get_value(self): + """Return modified Dataframe -- this is *not* a copy""" + # It is import to avoid accessing Qt C++ object as it has probably + # already been destroyed, due to the Qt.WA_DeleteOnClose attribute + df = self.dataModel.get_data() + if self.is_series: + return df.iloc[:, 0] + else: + return df + + def resize_to_contents(self): + QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) + self.dataTable.resizeColumnsToContents() + self.dataModel.fetch_more(columns=True) + self.dataTable.resizeColumnsToContents() + QApplication.restoreOverrideCursor() + + +#============================================================================== +# Tests +#============================================================================== +def test_edit(data, title="", parent=None): + """Test subroutine""" + app = qapplication() # analysis:ignore + dlg = DataFrameEditor(parent=parent) + + if dlg.setup_and_check(data, title=title): + dlg.exec_() + return dlg.get_value() + else: + import sys + sys.exit(1) + + +def test(): + """DataFrame editor test""" + from numpy import nan + from pandas.util.testing import assert_frame_equal, assert_series_equal + + df1 = DataFrame([ + [True, "bool"], + [1+1j, "complex"], + ['test', "string"], + [1.11, "float"], + [1, "int"], + [np.random.rand(3, 3), "Unkown type"], + ["Large value", 100], + ["áéí", "unicode"] + ], + index=['a', 'b', nan, nan, nan, 'c', + "Test global max", 'd'], + columns=[nan, 'Type']) + out = test_edit(df1) + assert_frame_equal(df1, out) + + result = Series([True, "bool"], index=[nan, 'Type'], name='a') + out = test_edit(df1.iloc[0]) + assert_series_equal(result, out) + + # Sorting large DataFrame takes time + df1 = DataFrame(np.random.rand(100100, 10)) + df1.sort(columns=[0, 1], inplace=True) + out = test_edit(df1) + assert_frame_equal(out, df1) + + series = Series(np.arange(10), name=0) + out = test_edit(series) + assert_series_equal(series, out) + + +if __name__ == '__main__': + test() diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/widgets/variableexplorer/importwizard.py spyder-3.0.2+dfsg1/spyder/widgets/variableexplorer/importwizard.py --- spyder-2.3.8+dfsg1/spyder/widgets/variableexplorer/importwizard.py 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/widgets/variableexplorer/importwizard.py 2016-10-25 02:05:23.000000000 +0200 @@ -0,0 +1,641 @@ +# -*- coding: utf-8 -*- +# +# Copyright © Spyder Project Contributors +# Licensed under the terms of the MIT License +# (see spyder/__init__.py for details) + +""" +Text data Importing Wizard based on Qt +""" + +# Standard library imports +from __future__ import print_function +from functools import partial as ft_partial + +# Third party imports +from qtpy.compat import to_qvariant +from qtpy.QtCore import QAbstractTableModel, QModelIndex, Qt, Signal, Slot +from qtpy.QtGui import QColor, QIntValidator +from qtpy.QtWidgets import (QCheckBox, QDialog, QFrame, QGridLayout, QGroupBox, + QHBoxLayout, QLabel, QLineEdit, + QPushButton, QMenu, QMessageBox, QRadioButton, + QSizePolicy, QSpacerItem, QTableView, QTabWidget, + QTextEdit, QVBoxLayout, QWidget) + +try: + import pandas as pd +except ImportError: + pd = None + +# Local import +from spyder.config.base import _ +from spyder.py3compat import (INT_TYPES, io, TEXT_TYPES, to_text_string, + zip_longest) +from spyder.utils import programs +from spyder.utils import icon_manager as ima +from spyder.utils.qthelpers import add_actions, create_action + + +def try_to_parse(value): + _types = ('int', 'float') + for _t in _types: + try: + _val = eval("%s('%s')" % (_t, value)) + return _val + except (ValueError, SyntaxError): + pass + return value + +def try_to_eval(value): + try: + return eval(value) + except (NameError, SyntaxError, ImportError): + return value + +#----Numpy arrays support +class FakeObject(object): + """Fake class used in replacement of missing modules""" + pass +try: + from numpy import ndarray, array +except ImportError: + class ndarray(FakeObject): # analysis:ignore + """Fake ndarray""" + pass + +#----date and datetime objects support +import datetime +try: + from dateutil.parser import parse as dateparse +except ImportError: + def dateparse(datestr, dayfirst=True): # analysis:ignore + """Just for 'day/month/year' strings""" + _a, _b, _c = list(map(int, datestr.split('/'))) + if dayfirst: + return datetime.datetime(_c, _b, _a) + return datetime.datetime(_c, _a, _b) + +def datestr_to_datetime(value, dayfirst=True): + return dateparse(value, dayfirst=dayfirst) + +#----Background colors for supported types +COLORS = { + bool: Qt.magenta, + tuple([float] + list(INT_TYPES)): Qt.blue, + list: Qt.yellow, + dict: Qt.cyan, + tuple: Qt.lightGray, + TEXT_TYPES: Qt.darkRed, + ndarray: Qt.green, + datetime.date: Qt.darkYellow, + } + +def get_color(value, alpha): + """Return color depending on value type""" + color = QColor() + for typ in COLORS: + if isinstance(value, typ): + color = QColor(COLORS[typ]) + color.setAlphaF(alpha) + return color + + +class ContentsWidget(QWidget): + """Import wizard contents widget""" + asDataChanged = Signal(bool) + + def __init__(self, parent, text): + QWidget.__init__(self, parent) + + self.text_editor = QTextEdit(self) + self.text_editor.setText(text) + self.text_editor.setReadOnly(True) + + # Type frame + type_layout = QHBoxLayout() + type_label = QLabel(_("Import as")) + type_layout.addWidget(type_label) + data_btn = QRadioButton(_("data")) + data_btn.setChecked(True) + self._as_data= True + type_layout.addWidget(data_btn) + code_btn = QRadioButton(_("code")) + self._as_code = False + type_layout.addWidget(code_btn) + txt_btn = QRadioButton(_("text")) + type_layout.addWidget(txt_btn) + + h_spacer = QSpacerItem(40, 20, + QSizePolicy.Expanding, QSizePolicy.Minimum) + type_layout.addItem(h_spacer) + type_frame = QFrame() + type_frame.setLayout(type_layout) + + # Opts frame + grid_layout = QGridLayout() + grid_layout.setSpacing(0) + + col_label = QLabel(_("Column separator:")) + grid_layout.addWidget(col_label, 0, 0) + col_w = QWidget() + col_btn_layout = QHBoxLayout() + self.tab_btn = QRadioButton(_("Tab")) + self.tab_btn.setChecked(False) + col_btn_layout.addWidget(self.tab_btn) + other_btn_col = QRadioButton(_("other")) + other_btn_col.setChecked(True) + col_btn_layout.addWidget(other_btn_col) + col_w.setLayout(col_btn_layout) + grid_layout.addWidget(col_w, 0, 1) + self.line_edt = QLineEdit(",") + self.line_edt.setMaximumWidth(30) + self.line_edt.setEnabled(True) + other_btn_col.toggled.connect(self.line_edt.setEnabled) + grid_layout.addWidget(self.line_edt, 0, 2) + + row_label = QLabel(_("Row separator:")) + grid_layout.addWidget(row_label, 1, 0) + row_w = QWidget() + row_btn_layout = QHBoxLayout() + self.eol_btn = QRadioButton(_("EOL")) + self.eol_btn.setChecked(True) + row_btn_layout.addWidget(self.eol_btn) + other_btn_row = QRadioButton(_("other")) + row_btn_layout.addWidget(other_btn_row) + row_w.setLayout(row_btn_layout) + grid_layout.addWidget(row_w, 1, 1) + self.line_edt_row = QLineEdit(";") + self.line_edt_row.setMaximumWidth(30) + self.line_edt_row.setEnabled(False) + other_btn_row.toggled.connect(self.line_edt_row.setEnabled) + grid_layout.addWidget(self.line_edt_row, 1, 2) + + grid_layout.setRowMinimumHeight(2, 15) + + other_group = QGroupBox(_("Additional options")) + other_layout = QGridLayout() + other_group.setLayout(other_layout) + + skiprows_label = QLabel(_("Skip rows:")) + other_layout.addWidget(skiprows_label, 0, 0) + self.skiprows_edt = QLineEdit('0') + self.skiprows_edt.setMaximumWidth(30) + intvalid = QIntValidator(0, len(to_text_string(text).splitlines()), + self.skiprows_edt) + self.skiprows_edt.setValidator(intvalid) + other_layout.addWidget(self.skiprows_edt, 0, 1) + + other_layout.setColumnMinimumWidth(2, 5) + + comments_label = QLabel(_("Comments:")) + other_layout.addWidget(comments_label, 0, 3) + self.comments_edt = QLineEdit('#') + self.comments_edt.setMaximumWidth(30) + other_layout.addWidget(self.comments_edt, 0, 4) + + self.trnsp_box = QCheckBox(_("Transpose")) + #self.trnsp_box.setEnabled(False) + other_layout.addWidget(self.trnsp_box, 1, 0, 2, 0) + + grid_layout.addWidget(other_group, 3, 0, 2, 0) + + opts_frame = QFrame() + opts_frame.setLayout(grid_layout) + + data_btn.toggled.connect(opts_frame.setEnabled) + data_btn.toggled.connect(self.set_as_data) + code_btn.toggled.connect(self.set_as_code) +# self.connect(txt_btn, SIGNAL("toggled(bool)"), +# self, SLOT("is_text(bool)")) + + # Final layout + layout = QVBoxLayout() + layout.addWidget(type_frame) + layout.addWidget(self.text_editor) + layout.addWidget(opts_frame) + self.setLayout(layout) + + def get_as_data(self): + """Return if data type conversion""" + return self._as_data + + def get_as_code(self): + """Return if code type conversion""" + return self._as_code + + def get_as_num(self): + """Return if numeric type conversion""" + return self._as_num + + def get_col_sep(self): + """Return the column separator""" + if self.tab_btn.isChecked(): + return u"\t" + return to_text_string(self.line_edt.text()) + + def get_row_sep(self): + """Return the row separator""" + if self.eol_btn.isChecked(): + return u"\n" + return to_text_string(self.line_edt_row.text()) + + def get_skiprows(self): + """Return number of lines to be skipped""" + return int(to_text_string(self.skiprows_edt.text())) + + def get_comments(self): + """Return comment string""" + return to_text_string(self.comments_edt.text()) + + @Slot(bool) + def set_as_data(self, as_data): + """Set if data type conversion""" + self._as_data = as_data + self.asDataChanged.emit(as_data) + + @Slot(bool) + def set_as_code(self, as_code): + """Set if code type conversion""" + self._as_code = as_code + + +class PreviewTableModel(QAbstractTableModel): + """Import wizard preview table model""" + def __init__(self, data=[], parent=None): + QAbstractTableModel.__init__(self, parent) + self._data = data + + def rowCount(self, parent=QModelIndex()): + """Return row count""" + return len(self._data) + + def columnCount(self, parent=QModelIndex()): + """Return column count""" + return len(self._data[0]) + + def _display_data(self, index): + """Return a data element""" + return to_qvariant(self._data[index.row()][index.column()]) + + def data(self, index, role=Qt.DisplayRole): + """Return a model data element""" + if not index.isValid(): + return to_qvariant() + if role == Qt.DisplayRole: + return self._display_data(index) + elif role == Qt.BackgroundColorRole: + return to_qvariant(get_color(self._data[index.row()][index.column()], .2)) + elif role == Qt.TextAlignmentRole: + return to_qvariant(int(Qt.AlignRight|Qt.AlignVCenter)) + return to_qvariant() + + def setData(self, index, value, role=Qt.EditRole): + """Set model data""" + return False + + def get_data(self): + """Return a copy of model data""" + return self._data[:][:] + + def parse_data_type(self, index, **kwargs): + """Parse a type to an other type""" + if not index.isValid(): + return False + try: + if kwargs['atype'] == "date": + self._data[index.row()][index.column()] = \ + datestr_to_datetime(self._data[index.row()][index.column()], + kwargs['dayfirst']).date() + elif kwargs['atype'] == "perc": + _tmp = self._data[index.row()][index.column()].replace("%", "") + self._data[index.row()][index.column()] = eval(_tmp)/100. + elif kwargs['atype'] == "account": + _tmp = self._data[index.row()][index.column()].replace(",", "") + self._data[index.row()][index.column()] = eval(_tmp) + elif kwargs['atype'] == "unicode": + self._data[index.row()][index.column()] = to_text_string( + self._data[index.row()][index.column()]) + elif kwargs['atype'] == "int": + self._data[index.row()][index.column()] = int( + self._data[index.row()][index.column()]) + elif kwargs['atype'] == "float": + self._data[index.row()][index.column()] = float( + self._data[index.row()][index.column()]) + self.dataChanged.emit(index, index) + except Exception as instance: + print(instance) + + def reset(self): + self.beginResetModel() + self.endResetModel() + +class PreviewTable(QTableView): + """Import wizard preview widget""" + def __init__(self, parent): + QTableView.__init__(self, parent) + self._model = None + + # Setting up actions + self.date_dayfirst_action = create_action(self, "dayfirst", + triggered=ft_partial(self.parse_to_type, atype="date", dayfirst=True)) + self.date_monthfirst_action = create_action(self, "monthfirst", + triggered=ft_partial(self.parse_to_type, atype="date", dayfirst=False)) + self.perc_action = create_action(self, "perc", + triggered=ft_partial(self.parse_to_type, atype="perc")) + self.acc_action = create_action(self, "account", + triggered=ft_partial(self.parse_to_type, atype="account")) + self.str_action = create_action(self, "unicode", + triggered=ft_partial(self.parse_to_type, atype="unicode")) + self.int_action = create_action(self, "int", + triggered=ft_partial(self.parse_to_type, atype="int")) + self.float_action = create_action(self, "float", + triggered=ft_partial(self.parse_to_type, atype="float")) + + # Setting up menus + self.date_menu = QMenu() + self.date_menu.setTitle("Date") + add_actions( self.date_menu, (self.date_dayfirst_action, + self.date_monthfirst_action)) + self.parse_menu = QMenu(self) + self.parse_menu.addMenu(self.date_menu) + add_actions( self.parse_menu, (self.perc_action, self.acc_action)) + self.parse_menu.setTitle("String to") + self.opt_menu = QMenu(self) + self.opt_menu.addMenu(self.parse_menu) + add_actions( self.opt_menu, (self.str_action, self.int_action, + self.float_action)) + + def _shape_text(self, text, colsep=u"\t", rowsep=u"\n", + transpose=False, skiprows=0, comments='#'): + """Decode the shape of the given text""" + assert colsep != rowsep + out = [] + text_rows = text.split(rowsep)[skiprows:] + for row in text_rows: + stripped = to_text_string(row).strip() + if len(stripped) == 0 or stripped.startswith(comments): + continue + line = to_text_string(row).split(colsep) + line = [try_to_parse(to_text_string(x)) for x in line] + out.append(line) + # Replace missing elements with np.nan's or None's + if programs.is_module_installed('numpy'): + from numpy import nan + out = list(zip_longest(*out, fillvalue=nan)) + else: + out = list(zip_longest(*out, fillvalue=None)) + # Tranpose the last result to get the expected one + out = [[r[col] for r in out] for col in range(len(out[0]))] + if transpose: + return [[r[col] for r in out] for col in range(len(out[0]))] + return out + + def get_data(self): + """Return model data""" + if self._model is None: + return None + return self._model.get_data() + + def process_data(self, text, colsep=u"\t", rowsep=u"\n", + transpose=False, skiprows=0, comments='#'): + """Put data into table model""" + data = self._shape_text(text, colsep, rowsep, transpose, skiprows, + comments) + self._model = PreviewTableModel(data) + self.setModel(self._model) + + @Slot() + def parse_to_type(self,**kwargs): + """Parse to a given type""" + indexes = self.selectedIndexes() + if not indexes: return + for index in indexes: + self.model().parse_data_type(index, **kwargs) + + def contextMenuEvent(self, event): + """Reimplement Qt method""" + self.opt_menu.popup(event.globalPos()) + event.accept() + + +class PreviewWidget(QWidget): + """Import wizard preview widget""" + + def __init__(self, parent): + QWidget.__init__(self, parent) + + vert_layout = QVBoxLayout() + + # Type frame + type_layout = QHBoxLayout() + type_label = QLabel(_("Import as")) + type_layout.addWidget(type_label) + + self.array_btn = array_btn = QRadioButton(_("array")) + array_btn.setEnabled(ndarray is not FakeObject) + array_btn.setChecked(ndarray is not FakeObject) + type_layout.addWidget(array_btn) + + list_btn = QRadioButton(_("list")) + list_btn.setChecked(not array_btn.isChecked()) + type_layout.addWidget(list_btn) + + if pd: + self.df_btn = df_btn = QRadioButton(_("DataFrame")) + df_btn.setChecked(False) + type_layout.addWidget(df_btn) + + h_spacer = QSpacerItem(40, 20, + QSizePolicy.Expanding, QSizePolicy.Minimum) + type_layout.addItem(h_spacer) + type_frame = QFrame() + type_frame.setLayout(type_layout) + + self._table_view = PreviewTable(self) + vert_layout.addWidget(type_frame) + vert_layout.addWidget(self._table_view) + self.setLayout(vert_layout) + + def open_data(self, text, colsep=u"\t", rowsep=u"\n", + transpose=False, skiprows=0, comments='#'): + """Open clipboard text as table""" + if pd: + self.pd_text = text + self.pd_info = dict(sep=colsep, lineterminator=rowsep, + skiprows=skiprows,comment=comments) + self._table_view.process_data(text, colsep, rowsep, transpose, + skiprows, comments) + + def get_data(self): + """Return table data""" + return self._table_view.get_data() + + +class ImportWizard(QDialog): + """Text data import wizard""" + def __init__(self, parent, text, + title=None, icon=None, contents_title=None, varname=None): + QDialog.__init__(self, parent) + + # Destroying the C++ object right after closing the dialog box, + # otherwise it may be garbage-collected in another QThread + # (e.g. the editor's analysis thread in Spyder), thus leading to + # a segmentation fault on UNIX or an application crash on Windows + self.setAttribute(Qt.WA_DeleteOnClose) + + if title is None: + title = _("Import wizard") + self.setWindowTitle(title) + if icon is None: + self.setWindowIcon(ima.icon('fileimport')) + if contents_title is None: + contents_title = _("Raw text") + + if varname is None: + varname = _("variable_name") + + self.var_name, self.clip_data = None, None + + # Setting GUI + self.tab_widget = QTabWidget(self) + self.text_widget = ContentsWidget(self, text) + self.table_widget = PreviewWidget(self) + + self.tab_widget.addTab(self.text_widget, _("text")) + self.tab_widget.setTabText(0, contents_title) + self.tab_widget.addTab(self.table_widget, _("table")) + self.tab_widget.setTabText(1, _("Preview")) + self.tab_widget.setTabEnabled(1, False) + + name_layout = QHBoxLayout() + name_label = QLabel(_("Variable Name")) + name_layout.addWidget(name_label) + + self.name_edt = QLineEdit() + self.name_edt.setText(varname) + name_layout.addWidget(self.name_edt) + + btns_layout = QHBoxLayout() + cancel_btn = QPushButton(_("Cancel")) + btns_layout.addWidget(cancel_btn) + cancel_btn.clicked.connect(self.reject) + h_spacer = QSpacerItem(40, 20, + QSizePolicy.Expanding, QSizePolicy.Minimum) + btns_layout.addItem(h_spacer) + self.back_btn = QPushButton(_("Previous")) + self.back_btn.setEnabled(False) + btns_layout.addWidget(self.back_btn) + self.back_btn.clicked.connect(ft_partial(self._set_step, step=-1)) + self.fwd_btn = QPushButton(_("Next")) + if not text: + self.fwd_btn.setEnabled(False) + btns_layout.addWidget(self.fwd_btn) + self.fwd_btn.clicked.connect(ft_partial(self._set_step, step=1)) + self.done_btn = QPushButton(_("Done")) + self.done_btn.setEnabled(False) + btns_layout.addWidget(self.done_btn) + self.done_btn.clicked.connect(self.process) + + self.text_widget.asDataChanged.connect(self.fwd_btn.setEnabled) + self.text_widget.asDataChanged.connect(self.done_btn.setDisabled) + layout = QVBoxLayout() + layout.addLayout(name_layout) + layout.addWidget(self.tab_widget) + layout.addLayout(btns_layout) + self.setLayout(layout) + + def _focus_tab(self, tab_idx): + """Change tab focus""" + for i in range(self.tab_widget.count()): + self.tab_widget.setTabEnabled(i, False) + self.tab_widget.setTabEnabled(tab_idx, True) + self.tab_widget.setCurrentIndex(tab_idx) + + def _set_step(self, step): + """Proceed to a given step""" + new_tab = self.tab_widget.currentIndex() + step + assert new_tab < self.tab_widget.count() and new_tab >= 0 + if new_tab == self.tab_widget.count()-1: + try: + self.table_widget.open_data(self._get_plain_text(), + self.text_widget.get_col_sep(), + self.text_widget.get_row_sep(), + self.text_widget.trnsp_box.isChecked(), + self.text_widget.get_skiprows(), + self.text_widget.get_comments()) + self.done_btn.setEnabled(True) + self.done_btn.setDefault(True) + self.fwd_btn.setEnabled(False) + self.back_btn.setEnabled(True) + except (SyntaxError, AssertionError) as error: + QMessageBox.critical(self, _("Import wizard"), + _("Unable to proceed to next step" + "

    Please check your entries." + "

    Error message:
    %s") % str(error)) + return + elif new_tab == 0: + self.done_btn.setEnabled(False) + self.fwd_btn.setEnabled(True) + self.back_btn.setEnabled(False) + self._focus_tab(new_tab) + + def get_data(self): + """Return processed data""" + # It is import to avoid accessing Qt C++ object as it has probably + # already been destroyed, due to the Qt.WA_DeleteOnClose attribute + return self.var_name, self.clip_data + + def _simplify_shape(self, alist, rec=0): + """Reduce the alist dimension if needed""" + if rec != 0: + if len(alist) == 1: + return alist[-1] + return alist + if len(alist) == 1: + return self._simplify_shape(alist[-1], 1) + return [self._simplify_shape(al, 1) for al in alist] + + def _get_table_data(self): + """Return clipboard processed as data""" + data = self._simplify_shape( + self.table_widget.get_data()) + if self.table_widget.array_btn.isChecked(): + return array(data) + elif pd and self.table_widget.df_btn.isChecked(): + info = self.table_widget.pd_info + buf = io.StringIO(self.table_widget.pd_text) + return pd.read_csv(buf, **info) + return data + + def _get_plain_text(self): + """Return clipboard as text""" + return self.text_widget.text_editor.toPlainText() + + @Slot() + def process(self): + """Process the data from clipboard""" + var_name = self.name_edt.text() + try: + self.var_name = str(var_name) + except UnicodeEncodeError: + self.var_name = to_text_string(var_name) + if self.text_widget.get_as_data(): + self.clip_data = self._get_table_data() + elif self.text_widget.get_as_code(): + self.clip_data = try_to_eval( + to_text_string(self._get_plain_text())) + else: + self.clip_data = to_text_string(self._get_plain_text()) + self.accept() + + +def test(text): + """Test""" + from spyder.utils.qthelpers import qapplication + _app = qapplication() # analysis:ignore + dialog = ImportWizard(None, text) + if dialog.exec_(): + print(dialog.get_data()) + +if __name__ == "__main__": + test(u"17/11/1976\t1.34\n14/05/09\t3.14") diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/widgets/variableexplorer/__init__.py spyder-3.0.2+dfsg1/spyder/widgets/variableexplorer/__init__.py --- spyder-2.3.8+dfsg1/spyder/widgets/variableexplorer/__init__.py 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/widgets/variableexplorer/__init__.py 2016-10-25 02:05:23.000000000 +0200 @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +# +# Copyright © Spyder Project Contributors +# Licensed under the terms of the MIT License +# (see spyder/__init__.py for details) + +""" +spyder.widgets.variableexplorer +=============================== + +Variable Explorer related widgets +""" diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/widgets/variableexplorer/namespacebrowser.py spyder-3.0.2+dfsg1/spyder/widgets/variableexplorer/namespacebrowser.py --- spyder-2.3.8+dfsg1/spyder/widgets/variableexplorer/namespacebrowser.py 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/widgets/variableexplorer/namespacebrowser.py 2016-11-17 04:39:40.000000000 +0100 @@ -0,0 +1,587 @@ +# -*- coding: utf-8 -*- +# +# Copyright © Spyder Project Contributors +# Licensed under the terms of the MIT License +# (see spyder/__init__.py for details) + +""" +Namespace browser widget + +This is the main widget used in the Variable Explorer plugin +""" + +# Standard library imports +import os.path as osp +import socket + +# Third library imports (qtpy) +from qtpy.compat import getsavefilename, getopenfilenames +from qtpy.QtCore import Qt, Signal, Slot +from qtpy.QtGui import QCursor +from qtpy.QtWidgets import (QApplication, QHBoxLayout, QInputDialog, QMenu, + QMessageBox, QToolButton, QVBoxLayout, QWidget) + +# Third party imports (others) +try: + import ipykernel.pickleutil + from ipykernel.serialize import serialize_object +except ImportError: + serialize_object = None + +# Local imports +from spyder.config.base import _, get_supported_types +from spyder.py3compat import is_text_string, getcwd, to_text_string +from spyder.utils import encoding +from spyder.utils import icon_manager as ima +from spyder.utils.iofuncs import iofunctions +from spyder.utils.misc import fix_reference_name +from spyder.utils.programs import is_module_installed +from spyder.utils.qthelpers import (add_actions, create_action, + create_toolbutton) +from spyder.widgets.externalshell.monitor import ( + communicate, monitor_copy_global, monitor_del_global, monitor_get_global, + monitor_load_globals, monitor_save_globals, monitor_set_global) +from spyder.widgets.variableexplorer.collectionseditor import ( + RemoteCollectionsEditorTableView) +from spyder.widgets.variableexplorer.importwizard import ImportWizard +from spyder.widgets.variableexplorer.utils import REMOTE_SETTINGS + + +SUPPORTED_TYPES = get_supported_types() + +# XXX --- Disable canning for Numpy arrays for now --- +# This allows getting values between a Python 3 frontend +# and a Python 2 kernel, and viceversa, for several types of +# arrays. +# See this link for interesting ideas on how to solve this +# in the future: +# http://stackoverflow.com/q/30698004/438386 +if serialize_object is not None: + ipykernel.pickleutil.can_map.pop('numpy.ndarray') + + +class NamespaceBrowser(QWidget): + """Namespace browser (global variables explorer widget)""" + sig_option_changed = Signal(str, object) + sig_collapse = Signal() + + def __init__(self, parent): + QWidget.__init__(self, parent) + + self.shellwidget = None + self.is_visible = True + self.setup_in_progress = None + + # Remote dict editor settings + self.check_all = None + self.exclude_private = None + self.exclude_uppercase = None + self.exclude_capitalized = None + self.exclude_unsupported = None + self.excluded_names = None + self.minmax = None + self.remote_editing = None + self.autorefresh = None + + self.editor = None + self.exclude_private_action = None + self.exclude_uppercase_action = None + self.exclude_capitalized_action = None + self.exclude_unsupported_action = None + + self.filename = None + + # For IPython clients + self.is_ipyclient = False + self.var_properties = {} + + def setup(self, check_all=None, exclude_private=None, + exclude_uppercase=None, exclude_capitalized=None, + exclude_unsupported=None, excluded_names=None, + minmax=None, remote_editing=None, + autorefresh=None): + """Setup the namespace browser""" + assert self.shellwidget is not None + + self.check_all = check_all + self.exclude_private = exclude_private + self.exclude_uppercase = exclude_uppercase + self.exclude_capitalized = exclude_capitalized + self.exclude_unsupported = exclude_unsupported + self.excluded_names = excluded_names + self.minmax = minmax + self.remote_editing = remote_editing + self.autorefresh = autorefresh + + if self.editor is not None: + self.editor.setup_menu(minmax) + self.exclude_private_action.setChecked(exclude_private) + self.exclude_uppercase_action.setChecked(exclude_uppercase) + self.exclude_capitalized_action.setChecked(exclude_capitalized) + self.exclude_unsupported_action.setChecked(exclude_unsupported) + if self.auto_refresh_button is not None: + self.auto_refresh_button.setChecked(autorefresh) + self.refresh_table() + return + + self.editor = RemoteCollectionsEditorTableView(self, None, + minmax=minmax, + remote_editing=remote_editing, + get_value_func=self.get_value, + set_value_func=self.set_value, + new_value_func=self.set_value, + remove_values_func=self.remove_values, + copy_value_func=self.copy_value, + is_list_func=self.is_list, + get_len_func=self.get_len, + is_array_func=self.is_array, + is_image_func=self.is_image, + is_dict_func=self.is_dict, + is_data_frame_func=self.is_data_frame, + is_series_func=self.is_series, + get_array_shape_func=self.get_array_shape, + get_array_ndim_func=self.get_array_ndim, + oedit_func=self.oedit, + plot_func=self.plot, imshow_func=self.imshow, + show_image_func=self.show_image) + self.editor.sig_option_changed.connect(self.sig_option_changed.emit) + self.editor.sig_files_dropped.connect(self.import_data) + + # Setup layout + layout = QVBoxLayout() + blayout = QHBoxLayout() + toolbar = self.setup_toolbar(exclude_private, exclude_uppercase, + exclude_capitalized, exclude_unsupported, + autorefresh) + for widget in toolbar: + blayout.addWidget(widget) + + # Options menu + options_button = create_toolbutton(self, text=_('Options'), + icon=ima.icon('tooloptions')) + options_button.setPopupMode(QToolButton.InstantPopup) + menu = QMenu(self) + editor = self.editor + actions = [self.exclude_private_action, self.exclude_uppercase_action, + self.exclude_capitalized_action, + self.exclude_unsupported_action, None] + if is_module_installed('numpy'): + actions.append(editor.minmax_action) + add_actions(menu, actions) + options_button.setMenu(menu) + + blayout.addStretch() + blayout.addWidget(options_button) + layout.addLayout(blayout) + layout.addWidget(self.editor) + self.setLayout(layout) + layout.setContentsMargins(0, 0, 0, 0) + + self.sig_option_changed.connect(self.option_changed) + + def set_shellwidget(self, shellwidget): + """Bind shellwidget instance to namespace browser""" + self.shellwidget = shellwidget + shellwidget.set_namespacebrowser(self) + + def setup_toolbar(self, exclude_private, exclude_uppercase, + exclude_capitalized, exclude_unsupported, autorefresh): + """Setup toolbar""" + self.setup_in_progress = True + + toolbar = [] + + # There is no need of refreshes for ipyclients + if not self.is_ipyclient: + refresh_button = create_toolbutton(self, text=_('Refresh'), + icon=ima.icon('reload'), + triggered=self.refresh_table) + self.auto_refresh_button = create_toolbutton(self, + text=_('Refresh periodically'), + icon=ima.icon('auto_reload'), + toggled=self.toggle_auto_refresh) + self.auto_refresh_button.setChecked(autorefresh) + else: + refresh_button = self.auto_refresh_button = None + + load_button = create_toolbutton(self, text=_('Import data'), + icon=ima.icon('fileimport'), + triggered=lambda: self.import_data()) + self.save_button = create_toolbutton(self, text=_("Save data"), + icon=ima.icon('filesave'), + triggered=lambda: self.save_data(self.filename)) + self.save_button.setEnabled(False) + save_as_button = create_toolbutton(self, + text=_("Save data as..."), + icon=ima.icon('filesaveas'), + triggered=self.save_data) + + if self.is_ipyclient: + toolbar += [load_button, self.save_button, save_as_button] + else: + toolbar += [refresh_button, self.auto_refresh_button, load_button, + self.save_button, save_as_button] + + self.exclude_private_action = create_action(self, + _("Exclude private references"), + tip=_("Exclude references which name starts" + " with an underscore"), + toggled=lambda state: + self.sig_option_changed.emit('exclude_private', state)) + self.exclude_private_action.setChecked(exclude_private) + + self.exclude_uppercase_action = create_action(self, + _("Exclude all-uppercase references"), + tip=_("Exclude references which name is uppercase"), + toggled=lambda state: + self.sig_option_changed.emit('exclude_uppercase', state)) + self.exclude_uppercase_action.setChecked(exclude_uppercase) + + self.exclude_capitalized_action = create_action(self, + _("Exclude capitalized references"), + tip=_("Exclude references which name starts with an " + "uppercase character"), + toggled=lambda state: + self.sig_option_changed.emit('exclude_capitalized', state)) + self.exclude_capitalized_action.setChecked(exclude_capitalized) + + self.exclude_unsupported_action = create_action(self, + _("Exclude unsupported data types"), + tip=_("Exclude references to unsupported data types" + " (i.e. which won't be handled/saved correctly)"), + toggled=lambda state: + self.sig_option_changed.emit('exclude_unsupported', state)) + self.exclude_unsupported_action.setChecked(exclude_unsupported) + + self.setup_in_progress = False + + return toolbar + + def option_changed(self, option, value): + """Option has changed""" + setattr(self, to_text_string(option), value) + if self.is_ipyclient: + self.shellwidget.set_namespace_view_settings() + self.refresh_table() + else: + settings = self.get_view_settings() + communicate(self._get_sock(), + 'set_remote_view_settings()', settings=[settings]) + + def visibility_changed(self, enable): + """Notify the widget whether its container (the namespace browser + plugin is visible or not""" + # This is slowing down Spyder a lot if too much data is present in + # the Variable Explorer, and users give focus to it after being hidden. + # This also happens when the Variable Explorer is visible and users + # give focus to Spyder after using another application (like Chrome + # or Firefox). + # That's why we've decided to remove this feature + # Fixes Issue 2593 + # + # self.is_visible = enable + # if enable: + # self.refresh_table() + pass + + @Slot(bool) + def toggle_auto_refresh(self, state): + """Toggle auto refresh state""" + self.autorefresh = state + if not self.setup_in_progress and not self.is_ipyclient: + communicate(self._get_sock(), + "set_monitor_auto_refresh(%r)" % state) + + def _get_sock(self): + """Return socket connection""" + return self.shellwidget.introspection_socket + + def get_view_settings(self): + """Return dict editor view settings""" + settings = {} + for name in REMOTE_SETTINGS: + settings[name] = getattr(self, name) + return settings + + @Slot() + def refresh_table(self): + """Refresh variable table""" + if self.is_visible and self.isVisible(): + if self.is_ipyclient: + self.shellwidget.refresh_namespacebrowser() + else: + if self.shellwidget.is_running(): + sock = self._get_sock() + if sock is None: + return + try: + communicate(sock, "refresh()") + except socket.error: + # Process was terminated before calling this method + pass + + def process_remote_view(self, remote_view): + """Process remote view""" + if remote_view is not None: + self.set_data(remote_view) + + def set_var_properties(self, properties): + """Set properties of variables""" + self.var_properties = properties + + #------ Remote commands ------------------------------------ + def get_value(self, name): + if self.is_ipyclient: + value = self.shellwidget.get_value(name) + + # Reset temporal variable where value is saved to + # save memory + self.shellwidget._kernel_value = None + else: + value = monitor_get_global(self._get_sock(), name) + if value is None: + if communicate(self._get_sock(), '%s is not None' % name): + import pickle + msg = to_text_string(_("Object %s is not picklable") + % name) + raise pickle.PicklingError(msg) + return value + + def set_value(self, name, value): + if self.is_ipyclient: + value = serialize_object(value) + self.shellwidget.set_value(name, value) + else: + monitor_set_global(self._get_sock(), name, value) + self.refresh_table() + + def remove_values(self, names): + for name in names: + if self.is_ipyclient: + self.shellwidget.remove_value(name) + else: + monitor_del_global(self._get_sock(), name) + self.refresh_table() + + def copy_value(self, orig_name, new_name): + if self.is_ipyclient: + self.shellwidget.copy_value(orig_name, new_name) + else: + monitor_copy_global(self._get_sock(), orig_name, new_name) + self.refresh_table() + + def is_list(self, name): + """Return True if variable is a list or a tuple""" + if self.is_ipyclient: + return self.var_properties[name]['is_list'] + else: + return communicate(self._get_sock(), + 'isinstance(%s, (tuple, list))' % name) + + def is_dict(self, name): + """Return True if variable is a dictionary""" + if self.is_ipyclient: + return self.var_properties[name]['is_dict'] + else: + return communicate(self._get_sock(), 'isinstance(%s, dict)' % name) + + def get_len(self, name): + """Return sequence length""" + if self.is_ipyclient: + return self.var_properties[name]['len'] + else: + return communicate(self._get_sock(), "len(%s)" % name) + + def is_array(self, name): + """Return True if variable is a NumPy array""" + if self.is_ipyclient: + return self.var_properties[name]['is_array'] + else: + return communicate(self._get_sock(), 'is_array("%s")' % name) + + def is_image(self, name): + """Return True if variable is a PIL.Image image""" + if self.is_ipyclient: + return self.var_properties[name]['is_image'] + else: + return communicate(self._get_sock(), 'is_image("%s")' % name) + + def is_data_frame(self, name): + """Return True if variable is a DataFrame""" + if self.is_ipyclient: + return self.var_properties[name]['is_data_frame'] + else: + return communicate(self._get_sock(), + "isinstance(globals()['%s'], DataFrame)" % name) + + def is_series(self, name): + """Return True if variable is a Series""" + if self.is_ipyclient: + return self.var_properties[name]['is_series'] + else: + return communicate(self._get_sock(), + "isinstance(globals()['%s'], Series)" % name) + + def get_array_shape(self, name): + """Return array's shape""" + if self.is_ipyclient: + return self.var_properties[name]['array_shape'] + else: + return communicate(self._get_sock(), "%s.shape" % name) + + def get_array_ndim(self, name): + """Return array's ndim""" + if self.is_ipyclient: + return self.var_properties[name]['array_ndim'] + else: + return communicate(self._get_sock(), "%s.ndim" % name) + + def plot(self, name, funcname): + if self.is_ipyclient: + self.shellwidget.execute("%%varexp --%s %s" % (funcname, name)) + else: + command = "import spyder.pyplot; "\ + "__fig__ = spyder.pyplot.figure(); "\ + "__items__ = getattr(spyder.pyplot, '%s')(%s); "\ + "spyder.pyplot.show(); "\ + "del __fig__, __items__;" % (funcname, name) + self.shellwidget.send_to_process(command) + + def imshow(self, name): + if self.is_ipyclient: + self.shellwidget.execute("%%varexp --imshow %s" % name) + else: + command = "import spyder.pyplot; " \ + "__fig__ = spyder.pyplot.figure(); " \ + "__items__ = spyder.pyplot.imshow(%s); " \ + "spyder.pyplot.show(); del __fig__, __items__;" % name + self.shellwidget.send_to_process(command) + + def show_image(self, name): + command = "%s.show()" % name + if self.is_ipyclient: + self.shellwidget.execute(command) + else: + self.shellwidget.send_to_process(command) + + def oedit(self, name): + command = "from spyder.widgets.variableexplorer.objecteditor import oedit; " \ + "oedit('%s', modal=False, namespace=locals());" % name + self.shellwidget.send_to_process(command) + + #------ Set, load and save data ------------------------------------------- + def set_data(self, data): + """Set data""" + if data != self.editor.model.get_data(): + self.editor.set_data(data) + self.editor.adjust_columns() + + def collapse(self): + """Collapse""" + self.sig_collapse.emit() + + @Slot(bool) + @Slot(list) + def import_data(self, filenames=None): + """Import data from text file""" + title = _("Import data") + if filenames is None: + if self.filename is None: + basedir = getcwd() + else: + basedir = osp.dirname(self.filename) + filenames, _selfilter = getopenfilenames(self, title, basedir, + iofunctions.load_filters) + if not filenames: + return + elif is_text_string(filenames): + filenames = [filenames] + + for filename in filenames: + self.filename = to_text_string(filename) + ext = osp.splitext(self.filename)[1].lower() + + if ext not in iofunctions.load_funcs: + buttons = QMessageBox.Yes | QMessageBox.Cancel + answer = QMessageBox.question(self, title, + _("Unsupported file extension '%s'

    " + "Would you like to import it anyway " + "(by selecting a known file format)?" + ) % ext, buttons) + if answer == QMessageBox.Cancel: + return + formats = list(iofunctions.load_extensions.keys()) + item, ok = QInputDialog.getItem(self, title, + _('Open file as:'), + formats, 0, False) + if ok: + ext = iofunctions.load_extensions[to_text_string(item)] + else: + return + + load_func = iofunctions.load_funcs[ext] + + # 'import_wizard' (self.setup_io) + if is_text_string(load_func): + # Import data with import wizard + error_message = None + try: + text, _encoding = encoding.read(self.filename) + base_name = osp.basename(self.filename) + editor = ImportWizard(self, text, title=base_name, + varname=fix_reference_name(base_name)) + if editor.exec_(): + var_name, clip_data = editor.get_data() + self.set_value(var_name, clip_data) + except Exception as error: + error_message = str(error) + else: + QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) + QApplication.processEvents() + if self.is_ipyclient: + error_message = self.shellwidget.load_data(self.filename, + ext) + self.shellwidget._kernel_reply = None + else: + error_message = monitor_load_globals(self._get_sock(), + self.filename, ext) + QApplication.restoreOverrideCursor() + QApplication.processEvents() + + if error_message is not None: + QMessageBox.critical(self, title, + _("Unable to load '%s'" + "

    Error message:
    %s" + ) % (self.filename, error_message)) + self.refresh_table() + + @Slot() + def save_data(self, filename=None): + """Save data""" + if filename is None: + filename = self.filename + if filename is None: + filename = getcwd() + filename, _selfilter = getsavefilename(self, _("Save data"), + filename, + iofunctions.save_filters) + if filename: + self.filename = filename + else: + return False + QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) + QApplication.processEvents() + if self.is_ipyclient: + error_message = self.shellwidget.save_namespace(self.filename) + self.shellwidget._kernel_reply = None + else: + settings = self.get_view_settings() + error_message = monitor_save_globals(self._get_sock(), settings, + filename) + QApplication.restoreOverrideCursor() + QApplication.processEvents() + if error_message is not None: + QMessageBox.critical(self, _("Save data"), + _("Unable to save current workspace" + "

    Error message:
    %s") % error_message) + self.save_button.setEnabled(self.filename is not None) diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/widgets/variableexplorer/objecteditor.py spyder-3.0.2+dfsg1/spyder/widgets/variableexplorer/objecteditor.py --- spyder-2.3.8+dfsg1/spyder/widgets/variableexplorer/objecteditor.py 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/widgets/variableexplorer/objecteditor.py 2016-10-25 02:05:23.000000000 +0200 @@ -0,0 +1,178 @@ +# -*- coding: utf-8 -*- +# +# Copyright © Spyder Project Contributors +# Licensed under the terms of the MIT License +# (see spyder/__init__.py for details) + +""" +Generic object editor dialog +""" + +# Standard library imports +from __future__ import print_function + +# Third party imports +from qtpy.QtCore import QObject + +# Local imports +from spyder.py3compat import is_text_string + + +class DialogKeeper(QObject): + def __init__(self): + QObject.__init__(self) + self.dialogs = {} + self.namespace = None + + def set_namespace(self, namespace): + self.namespace = namespace + + def create_dialog(self, dialog, refname, func): + self.dialogs[id(dialog)] = dialog, refname, func + dialog.accepted.connect( + lambda eid=id(dialog): self.editor_accepted(eid)) + dialog.rejected.connect( + lambda eid=id(dialog): self.editor_rejected(eid)) + dialog.show() + dialog.activateWindow() + dialog.raise_() + + def editor_accepted(self, dialog_id): + dialog, refname, func = self.dialogs[dialog_id] + self.namespace[refname] = func(dialog) + self.dialogs.pop(dialog_id) + + def editor_rejected(self, dialog_id): + self.dialogs.pop(dialog_id) + +keeper = DialogKeeper() + + +def create_dialog(obj, obj_name): + """Creates the editor dialog and returns a tuple (dialog, func) where func + is the function to be called with the dialog instance as argument, after + quitting the dialog box + + The role of this intermediate function is to allow easy monkey-patching. + (uschmitt suggested this indirection here so that he can monkey patch + oedit to show eMZed related data) + """ + # Local import + from spyder.widgets.variableexplorer.texteditor import TextEditor + from spyder.widgets.variableexplorer.utils import (ndarray, FakeObject, + Image, is_known_type, DataFrame, + Series) + from spyder.widgets.variableexplorer.collectionseditor import CollectionsEditor + from spyder.widgets.variableexplorer.arrayeditor import ArrayEditor + if DataFrame is not FakeObject: + from spyder.widgets.variableexplorer.dataframeeditor import DataFrameEditor + + conv_func = lambda data: data + readonly = not is_known_type(obj) + if isinstance(obj, ndarray) and ndarray is not FakeObject: + dialog = ArrayEditor() + if not dialog.setup_and_check(obj, title=obj_name, + readonly=readonly): + return + elif isinstance(obj, Image) and Image is not FakeObject \ + and ndarray is not FakeObject: + dialog = ArrayEditor() + import numpy as np + data = np.array(obj) + if not dialog.setup_and_check(data, title=obj_name, + readonly=readonly): + return + from spyder.pil_patch import Image + conv_func = lambda data: Image.fromarray(data, mode=obj.mode) + elif isinstance(obj, (DataFrame, Series)) and DataFrame is not FakeObject: + dialog = DataFrameEditor() + if not dialog.setup_and_check(obj): + return + elif is_text_string(obj): + dialog = TextEditor(obj, title=obj_name, readonly=readonly) + else: + dialog = CollectionsEditor() + dialog.setup(obj, title=obj_name, readonly=readonly) + + def end_func(dialog): + return conv_func(dialog.get_value()) + + return dialog, end_func + + +def oedit(obj, modal=True, namespace=None): + """Edit the object 'obj' in a GUI-based editor and return the edited copy + (if Cancel is pressed, return None) + + The object 'obj' is a container + + Supported container types: + dict, list, tuple, str/unicode or numpy.array + + (instantiate a new QApplication if necessary, + so it can be called directly from the interpreter) + """ + # Local import + from spyder.utils.qthelpers import qapplication + app = qapplication() + + if modal: + obj_name = '' + else: + assert is_text_string(obj) + obj_name = obj + if namespace is None: + namespace = globals() + keeper.set_namespace(namespace) + obj = namespace[obj_name] + # keep QApplication reference alive in the Python interpreter: + namespace['__qapp__'] = app + + result = create_dialog(obj, obj_name) + if result is None: + return + dialog, end_func = result + + if modal: + if dialog.exec_(): + return end_func(dialog) + else: + keeper.create_dialog(dialog, obj_name, end_func) + import os + if os.name == 'nt': + app.exec_() + + +#============================================================================== +# Tests +#============================================================================== +def test(): + """Run object editor test""" + import datetime, numpy as np + from spyder.pil_patch import Image + data = np.random.random_integers(255, size=(100, 100)).astype('uint8') + image = Image.fromarray(data) + example = {'str': 'kjkj kj k j j kj k jkj', + 'list': [1, 3, 4, 'kjkj', None], + 'dict': {'d': 1, 'a': np.random.rand(10, 10), 'b': [1, 2]}, + 'float': 1.2233, + 'array': np.random.rand(10, 10), + 'image': image, + 'date': datetime.date(1945, 5, 8), + 'datetime': datetime.datetime(1945, 5, 8), + } + image = oedit(image) + class Foobar(object): + def __init__(self): + self.text = "toto" + foobar = Foobar() + + print(oedit(foobar)) + print(oedit(example)) + print(oedit(np.random.rand(10, 10))) + print(oedit(oedit.__doc__)) + print(example) + + +if __name__ == "__main__": + test() diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/widgets/variableexplorer/texteditor.py spyder-3.0.2+dfsg1/spyder/widgets/variableexplorer/texteditor.py --- spyder-2.3.8+dfsg1/spyder/widgets/variableexplorer/texteditor.py 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/widgets/variableexplorer/texteditor.py 2016-10-25 02:05:23.000000000 +0200 @@ -0,0 +1,111 @@ +# -*- coding: utf-8 -*- +# +# Copyright © Spyder Project Contributors +# Licensed under the terms of the MIT License +# (see spyder/__init__.py for details) + +""" +Text editor dialog +""" + +# Standard library imports +from __future__ import print_function + +# Third party imports +from qtpy.QtCore import Qt +from qtpy.QtWidgets import QDialog, QDialogButtonBox, QTextEdit, QVBoxLayout + +# Local import +from spyder.config.base import _ +from spyder.config.gui import get_font +from spyder.py3compat import (is_binary_string, to_binary_string, + to_text_string) +from spyder.utils import icon_manager as ima + + +class TextEditor(QDialog): + """Array Editor Dialog""" + def __init__(self, text, title='', font=None, parent=None, + readonly=False, size=(400, 300)): + QDialog.__init__(self, parent) + + # Destroying the C++ object right after closing the dialog box, + # otherwise it may be garbage-collected in another QThread + # (e.g. the editor's analysis thread in Spyder), thus leading to + # a segmentation fault on UNIX or an application crash on Windows + self.setAttribute(Qt.WA_DeleteOnClose) + + self.text = None + + # Display text as unicode if it comes as bytes, so users see + # its right representation + if is_binary_string(text): + self.is_binary = True + text = to_text_string(text, 'utf8') + else: + self.is_binary = False + + self.layout = QVBoxLayout() + self.setLayout(self.layout) + + # Text edit + self.edit = QTextEdit(parent) + self.edit.textChanged.connect(self.text_changed) + self.edit.setReadOnly(readonly) + self.edit.setPlainText(text) + if font is None: + font = get_font() + self.edit.setFont(font) + self.layout.addWidget(self.edit) + + # Buttons configuration + buttons = QDialogButtonBox.Ok + if not readonly: + buttons = buttons | QDialogButtonBox.Cancel + bbox = QDialogButtonBox(buttons) + bbox.accepted.connect(self.accept) + bbox.rejected.connect(self.reject) + self.layout.addWidget(bbox) + + # Make the dialog act as a window + self.setWindowFlags(Qt.Window) + + self.setWindowIcon(ima.icon('edit')) + self.setWindowTitle(_("Text editor") + \ + "%s" % (" - "+str(title) if str(title) else "")) + self.resize(size[0], size[1]) + + def text_changed(self): + """Text has changed""" + # Save text as bytes, if it was initially bytes + if self.is_binary: + self.text = to_binary_string(self.edit.toPlainText(), 'utf8') + else: + self.text = to_text_string(self.edit.toPlainText()) + + def get_value(self): + """Return modified text""" + # It is import to avoid accessing Qt C++ object as it has probably + # already been destroyed, due to the Qt.WA_DeleteOnClose attribute + return self.text + + +#============================================================================== +# Tests +#============================================================================== +def test(): + """Text editor demo""" + from spyder.utils.qthelpers import qapplication + _app = qapplication() # analysis:ignore + + text = """01234567890123456789012345678901234567890123456789012345678901234567890123456789 +dedekdh elkd ezd ekjd lekdj elkdfjelfjk e""" + dialog = TextEditor(text) + dialog.exec_() + + dlg_text = dialog.get_value() + assert text == dlg_text + + +if __name__ == "__main__": + test() diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/widgets/variableexplorer/utils.py spyder-3.0.2+dfsg1/spyder/widgets/variableexplorer/utils.py --- spyder-2.3.8+dfsg1/spyder/widgets/variableexplorer/utils.py 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/widgets/variableexplorer/utils.py 2016-11-17 04:39:40.000000000 +0100 @@ -0,0 +1,488 @@ +# -*- coding: utf-8 -*- +# +# Copyright © Spyder Project Contributors +# Licensed under the terms of the MIT License +# (see spyder/__init__.py for details) + +""" +Utilities for the Collections editor widget and dialog +""" + +from __future__ import print_function + +import re + +# Local imports +from spyder.config.base import get_supported_types +from spyder.py3compat import (NUMERIC_TYPES, TEXT_TYPES, to_text_string, + is_text_string, is_binary_string, reprlib, + PY2, to_binary_string) +from spyder.utils import programs +from spyder import dependencies +from spyder.config.base import _ + + +#============================================================================== +# Dependencies +#============================================================================== +PANDAS_REQVER = '>=0.13.1' +dependencies.add('pandas', _("View and edit DataFrames and Series in the " + "Variable Explorer"), + required_version=PANDAS_REQVER, optional=True) + +NUMPY_REQVER = '>=1.7' +dependencies.add("numpy", _("View and edit two and three dimensional arrays " + "in the Variable Explorer"), + required_version=NUMPY_REQVER, optional=True) + +#============================================================================== +# FakeObject +#============================================================================== +class FakeObject(object): + """Fake class used in replacement of missing modules""" + pass + + +#============================================================================== +# Numpy arrays support +#============================================================================== +try: + from numpy import ndarray, array, matrix, recarray + from numpy.ma import MaskedArray + from numpy import savetxt as np_savetxt + from numpy import set_printoptions as np_set_printoptions +except ImportError: + ndarray = array = matrix = recarray = MaskedArray = np_savetxt = \ + np_set_printoptions = FakeObject + +def get_numpy_dtype(obj): + """Return NumPy data type associated to obj + Return None if NumPy is not available + or if obj is not a NumPy array or scalar""" + if ndarray is not FakeObject: + # NumPy is available + import numpy as np + if isinstance(obj, np.generic) or isinstance(obj, np.ndarray): + # Numpy scalars all inherit from np.generic. + # Numpy arrays all inherit from np.ndarray. + # If we check that we are certain we have one of these + # types then we are less likely to generate an exception below. + try: + return obj.dtype.type + except (AttributeError, RuntimeError): + # AttributeError: some NumPy objects have no dtype attribute + # RuntimeError: happens with NetCDF objects (Issue 998) + return + + +#============================================================================== +# Pandas support +#============================================================================== +if programs.is_module_installed('pandas', PANDAS_REQVER): + from pandas import DataFrame, Series +else: + DataFrame = Series = FakeObject # analysis:ignore + + +#============================================================================== +# PIL Images support +#============================================================================== +try: + from spyder import pil_patch + Image = pil_patch.Image.Image +except ImportError: + Image = FakeObject # analysis:ignore + + +#============================================================================== +# BeautifulSoup support (see Issue 2448) +#============================================================================== +try: + import bs4 + NavigableString = bs4.element.NavigableString +except (ImportError, AttributeError): + NavigableString = FakeObject # analysis:ignore + + +#============================================================================== +# Misc. +#============================================================================== +def address(obj): + """Return object address as a string: ''""" + return "<%s @ %s>" % (obj.__class__.__name__, + hex(id(obj)).upper().replace('X', 'x')) + + +def try_to_eval(value): + """Try to eval value""" + try: + return eval(value) + except (NameError, SyntaxError, ImportError): + return value + + +def get_size(item): + """Return size of an item of arbitrary type""" + if isinstance(item, (list, tuple, dict)): + return len(item) + elif isinstance(item, (ndarray, MaskedArray)): + return item.shape + elif isinstance(item, Image): + return item.size + if isinstance(item, (DataFrame, Series)): + return item.shape + else: + return 1 + + +#============================================================================== +# Set limits for the amount of elements in the repr of collections (lists, +# dicts, tuples and sets) and Numpy arrays +#============================================================================== +CollectionsRepr = reprlib.Repr() +CollectionsRepr.maxlist = 10 +CollectionsRepr.maxdict = 10 +CollectionsRepr.maxtuple = 10 +CollectionsRepr.maxset = 10 + +if np_set_printoptions is not FakeObject: + np_set_printoptions(threshold=10) + + +#============================================================================== +# Date and datetime objects support +#============================================================================== +import datetime + + +try: + from dateutil.parser import parse as dateparse +except ImportError: + def dateparse(datestr): # analysis:ignore + """Just for 'year, month, day' strings""" + return datetime.datetime( *list(map(int, datestr.split(','))) ) + + +def datestr_to_datetime(value): + rp = value.rfind('(')+1 + v = dateparse(value[rp:-1]) + print(value, "-->", v) + return v + + +#============================================================================== +# Background colors for supported types +#============================================================================== +ARRAY_COLOR = "#00ff00" +SCALAR_COLOR = "#0000ff" +COLORS = { + bool: "#ff00ff", + NUMERIC_TYPES: SCALAR_COLOR, + list: "#ffff00", + dict: "#00ffff", + tuple: "#c0c0c0", + TEXT_TYPES: "#800000", + (ndarray, + MaskedArray, + matrix, + DataFrame, + Series): ARRAY_COLOR, + Image: "#008000", + datetime.date: "#808000", + } +CUSTOM_TYPE_COLOR = "#7755aa" +UNSUPPORTED_COLOR = "#ffffff" + +def get_color_name(value): + """Return color name depending on value type""" + if not is_known_type(value): + return CUSTOM_TYPE_COLOR + for typ, name in list(COLORS.items()): + if isinstance(value, typ): + return name + else: + np_dtype = get_numpy_dtype(value) + if np_dtype is None or not hasattr(value, 'size'): + return UNSUPPORTED_COLOR + elif value.size == 1: + return SCALAR_COLOR + else: + return ARRAY_COLOR + + +def is_editable_type(value): + """Return True if data type is editable with a standard GUI-based editor, + like CollectionsEditor, ArrayEditor, QDateEdit or a simple QLineEdit""" + return get_color_name(value) not in (UNSUPPORTED_COLOR, CUSTOM_TYPE_COLOR) + + +#============================================================================== +# Sorting +#============================================================================== +def sort_against(list1, list2, reverse=False): + """ + Arrange items of list1 in the same order as sorted(list2). + + In other words, apply to list1 the permutation which takes list2 + to sorted(list2, reverse). + """ + try: + return [item for _, item in + sorted(zip(list2, list1), key=lambda x: x[0], reverse=reverse)] + except: + return list1 + + +def unsorted_unique(lista): + """Removes duplicates from lista neglecting its initial ordering""" + return list(set(lista)) + + +#============================================================================== +# Display <--> Value +#============================================================================== +def value_to_display(value, minmax=False): + """Convert value for display purpose""" + try: + if isinstance(value, recarray): + fields = value.names + display = 'Field names: ' + ', '.join(fields) + elif isinstance(value, MaskedArray): + display = 'Masked array' + elif isinstance(value, ndarray): + if minmax: + try: + display = 'Min: %r\nMax: %r' % (value.min(), value.max()) + except (TypeError, ValueError): + display = repr(value) + else: + display = repr(value) + elif isinstance(value, (list, tuple, dict, set)): + display = CollectionsRepr.repr(value) + elif isinstance(value, Image): + display = '%s Mode: %s' % (address(value), value.mode) + elif isinstance(value, DataFrame): + cols = value.columns + if PY2 and len(cols) > 0: + # Get rid of possible BOM utf-8 data present at the + # beginning of a file, which gets attached to the first + # column header when headers are present in the first + # row. + # Fixes Issue 2514 + try: + ini_col = to_text_string(cols[0], encoding='utf-8-sig') + except: + ini_col = to_text_string(cols[0]) + cols = [ini_col] + [to_text_string(c) for c in cols[1:]] + else: + cols = [to_text_string(c) for c in cols] + display = 'Column names: ' + ', '.join(list(cols)) + elif isinstance(value, NavigableString): + # Fixes Issue 2448 + display = to_text_string(value) + elif is_binary_string(value): + try: + display = to_text_string(value, 'utf8') + except: + display = value + elif is_text_string(value): + display = value + elif isinstance(value, NUMERIC_TYPES) or isinstance(value, bool) or \ + isinstance(value, datetime.date): + display = repr(value) + else: + # Note: Don't trust on repr's. They can be inefficient and + # so freeze Spyder quite easily + # display = repr(value) + type_str = to_text_string(type(value)) + display = type_str[1:-1] + except: + type_str = to_text_string(type(value)) + display = type_str[1:-1] + + # Truncate display at 80 chars to avoid freezing Spyder + # because of large displays + if len(display) > 80: + display = display[:80].rstrip() + ' ...' + + return display + + + +def display_to_value(value, default_value, ignore_errors=True): + """Convert back to value""" + from qtpy.compat import from_qvariant + value = from_qvariant(value, to_text_string) + try: + np_dtype = get_numpy_dtype(default_value) + if isinstance(default_value, bool): + # We must test for boolean before NumPy data types + # because `bool` class derives from `int` class + try: + value = bool(float(value)) + except ValueError: + value = value.lower() == "true" + elif np_dtype is not None: + if 'complex' in str(type(default_value)): + value = np_dtype(complex(value)) + else: + value = np_dtype(value) + elif is_binary_string(default_value): + value = to_binary_string(value, 'utf8') + elif is_text_string(default_value): + value = to_text_string(value) + elif isinstance(default_value, complex): + value = complex(value) + elif isinstance(default_value, float): + value = float(value) + elif isinstance(default_value, int): + try: + value = int(value) + except ValueError: + value = float(value) + elif isinstance(default_value, datetime.datetime): + value = datestr_to_datetime(value) + elif isinstance(default_value, datetime.date): + value = datestr_to_datetime(value).date() + elif ignore_errors: + value = try_to_eval(value) + else: + value = eval(value) + except (ValueError, SyntaxError): + if ignore_errors: + value = try_to_eval(value) + else: + return default_value + return value + + +#============================================================================== +# Types +#============================================================================== +def get_type_string(item): + """Return type string of an object""" + if isinstance(item, DataFrame): + return "DataFrame" + if isinstance(item, Series): + return "Series" + found = re.findall(r"<(?:type|class) '(\S*)'>", str(type(item))) + if found: + return found[0] + + +def is_known_type(item): + """Return True if object has a known type""" + # Unfortunately, the masked array case is specific + return isinstance(item, MaskedArray) or get_type_string(item) is not None + + +def get_human_readable_type(item): + """Return human-readable type string of an item""" + if isinstance(item, (ndarray, MaskedArray)): + return item.dtype.name + elif isinstance(item, Image): + return "Image" + else: + text = get_type_string(item) + if text is None: + text = to_text_string('unknown') + else: + return text[text.find('.')+1:] + + +#============================================================================== +# Globals filter: filter namespace dictionaries (to be edited in +# CollectionsEditor) +#============================================================================== +def is_supported(value, check_all=False, filters=None, iterate=True): + """Return True if the value is supported, False otherwise""" + assert filters is not None + if not is_editable_type(value): + return False + elif not isinstance(value, filters): + return False + elif iterate: + if isinstance(value, (list, tuple, set)): + for val in value: + if not is_supported(val, filters=filters, iterate=check_all): + return False + if not check_all: + break + elif isinstance(value, dict): + for key, val in list(value.items()): + if not is_supported(key, filters=filters, iterate=check_all) \ + or not is_supported(val, filters=filters, + iterate=check_all): + return False + if not check_all: + break + return True + + +def globalsfilter(input_dict, check_all=False, filters=None, + exclude_private=None, exclude_capitalized=None, + exclude_uppercase=None, exclude_unsupported=None, + excluded_names=None): + """Keep only objects that can be pickled""" + output_dict = {} + for key, value in list(input_dict.items()): + excluded = (exclude_private and key.startswith('_')) or \ + (exclude_capitalized and key[0].isupper()) or \ + (exclude_uppercase and key.isupper() + and len(key) > 1 and not key[1:].isdigit()) or \ + (key in excluded_names) or \ + (exclude_unsupported and \ + not is_supported(value, check_all=check_all, + filters=filters)) + if not excluded: + output_dict[key] = value + return output_dict + + +#============================================================================== +# Create view to be displayed by NamespaceBrowser +#============================================================================== +REMOTE_SETTINGS = ('check_all', 'exclude_private', 'exclude_uppercase', + 'exclude_capitalized', 'exclude_unsupported', + 'excluded_names', 'minmax', 'remote_editing', + 'autorefresh') + + +def get_remote_data(data, settings, mode, more_excluded_names=None): + """ + Return globals according to filter described in *settings*: + * data: data to be filtered (dictionary) + * settings: variable explorer settings (dictionary) + * mode (string): 'editable' or 'picklable' + * more_excluded_names: additional excluded names (list) + """ + supported_types = get_supported_types() + assert mode in list(supported_types.keys()) + excluded_names = settings['excluded_names'] + if more_excluded_names is not None: + excluded_names += more_excluded_names + return globalsfilter(data, check_all=settings['check_all'], + filters=tuple(supported_types[mode]), + exclude_private=settings['exclude_private'], + exclude_uppercase=settings['exclude_uppercase'], + exclude_capitalized=settings['exclude_capitalized'], + exclude_unsupported=settings['exclude_unsupported'], + excluded_names=excluded_names) + + +def make_remote_view(data, settings, more_excluded_names=None): + """ + Make a remote view of dictionary *data* + -> globals explorer + """ + assert all([name in REMOTE_SETTINGS for name in settings]) + data = get_remote_data(data, settings, mode='editable', + more_excluded_names=more_excluded_names) + remote = {} + for key, value in list(data.items()): + view = value_to_display(value, minmax=settings['minmax']) + remote[key] = {'type': get_human_readable_type(value), + 'size': get_size(value), + 'color': get_color_name(value), + 'view': view} + return remote diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder/workers/updates.py spyder-3.0.2+dfsg1/spyder/workers/updates.py --- spyder-2.3.8+dfsg1/spyder/workers/updates.py 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder/workers/updates.py 2016-10-25 02:05:23.000000000 +0200 @@ -0,0 +1,99 @@ +# -*- coding: utf-8 -*- +# +# Copyright © Spyder Project Contributors +# Licensed under the terms of the MIT License +# (see spyder/__init__.py for details) + +# Standard library imports +import json +import ssl + +# Third party imports +from qtpy.QtCore import QObject, Signal + +# Local imports +from spyder import __version__ +from spyder.config.base import _ +from spyder.py3compat import PY3 +from spyder.utils.programs import check_version, is_stable_version + + +if PY3: + from urllib.request import urlopen + from urllib.error import URLError, HTTPError +else: + from urllib2 import urlopen, URLError, HTTPError + + +class WorkerUpdates(QObject): + """ + Worker that checks for releases using the Github API without blocking the + Spyder user interface, in case of connections issues. + """ + sig_ready = Signal() + + def __init__(self, parent): + QObject.__init__(self) + self._parent = parent + self.error = None + self.latest_release = None + + def check_update_available(self, version, releases): + """Checks if there is an update available. + + It takes as parameters the current version of Spyder and a list of + valid cleaned releases in chronological order (what github api returns + by default). Example: ['2.3.4', '2.3.3' ...] + """ + if is_stable_version(version): + # Remove non stable versions from the list + releases = [r for r in releases if is_stable_version(r)] + + latest_release = releases[0] + + if version.endswith('dev'): + return (False, latest_release) + + return (check_version(version, latest_release, '<'), latest_release) + + def start(self): + """Main method of the WorkerUpdates worker""" + self.url = 'https://api.github.com/repos/spyder-ide/spyder/releases' + self.update_available = False + self.latest_release = __version__ + + error_msg = None + + try: + if hasattr(ssl, '_create_unverified_context'): + # Fix for issue # 2685 [Works only with Python >=2.7.9] + # More info: https://www.python.org/dev/peps/pep-0476/#opting-out + context = ssl._create_unverified_context() + page = urlopen(self.url, context=context) + else: + page = urlopen(self.url) + try: + data = page.read() + + # Needed step for python3 compatibility + if not isinstance(data, str): + data = data.decode() + + data = json.loads(data) + releases = [item['tag_name'].replace('v', '') for item in data] + version = __version__ + + result = self.check_update_available(version, releases) + self.update_available, self.latest_release = result + except Exception: + error_msg = _('Unable to retrieve information.') + except HTTPError: + error_msg = _('Unable to retrieve information.') + except URLError: + error_msg = _('Unable to connect to the internet.

    Make ' + 'sure the connection is working properly.') + except Exception: + error_msg = _('Unable to check for updates.') + + self.error = error_msg + self.sig_ready.emit() diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder_breakpoints/breakpoints.py spyder-3.0.2+dfsg1/spyder_breakpoints/breakpoints.py --- spyder-2.3.8+dfsg1/spyder_breakpoints/breakpoints.py 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder_breakpoints/breakpoints.py 2016-11-19 01:31:58.000000000 +0100 @@ -0,0 +1,104 @@ +# -*- coding:utf-8 -*- +# +# Copyright © Spyder Project Contributors +# Based loosely on p_pylint.py by Pierre Raybaut +# Licensed under the terms of the MIT License +# (see spyder/__init__.py for details) + +"""Breakpoint Plugin""" + +# pylint: disable=C0103 +# pylint: disable=R0903 +# pylint: disable=R0911 +# pylint: disable=R0201 + +# Standard library imports +import os.path as osp + +# Local imports +from spyder.config.base import get_translation +from spyder.utils import icon_manager as ima +from spyder.utils.qthelpers import create_action +from spyder.plugins import SpyderPluginMixin +from spyder.py3compat import to_text_string, is_text_string +from .widgets.breakpointsgui import BreakpointWidget + +_ = get_translation("breakpoints", "spyder_breakpoints") + + +class Breakpoints(BreakpointWidget, SpyderPluginMixin): + """Breakpoint list""" + CONF_SECTION = 'breakpoints' + +# CONFIGWIDGET_CLASS = BreakpointConfigPage + def __init__(self, parent=None): + BreakpointWidget.__init__(self, parent=parent) + SpyderPluginMixin.__init__(self, parent) + + # Initialize plugin + self.initialize_plugin() + self.set_data() + + #------ SpyderPluginWidget API -------------------------------------------- + def get_plugin_title(self): + """Return widget title""" + return _("Breakpoints") + + def get_plugin_icon(self): + """Return widget icon""" + path = osp.join(self.PLUGIN_PATH, self.IMG_PATH) + return ima.icon('profiler', icon_path=path) + + def get_focus_widget(self): + """ + Return the widget to give focus to when + this plugin's dockwidget is raised on top-level + """ + return self.dictwidget + + def get_plugin_actions(self): + """Return a list of actions related to plugin""" + return [] + + def on_first_registration(self): + """Action to be performed on first plugin registration""" + self.main.tabify_plugins(self.main.help, self) + + def register_plugin(self): + """Register plugin in Spyder's main window""" + self.edit_goto.connect(self.main.editor.load) + #self.redirect_stdio.connect(self.main.redirect_internalshell_stdio) + self.clear_all_breakpoints.connect( + self.main.editor.clear_all_breakpoints) + self.clear_breakpoint.connect(self.main.editor.clear_breakpoint) + self.main.editor.breakpoints_saved.connect(self.set_data) + self.set_or_edit_conditional_breakpoint.connect( + self.main.editor.set_or_edit_conditional_breakpoint) + + self.main.add_dockwidget(self) + + list_action = create_action(self, _("List breakpoints"), + triggered=self.show) + list_action.setEnabled(True) + pos = self.main.debug_menu_actions.index('list_breakpoints') + self.main.debug_menu_actions.insert(pos, list_action) + self.main.editor.pythonfile_dependent_actions += [list_action] + + def refresh_plugin(self): + """Refresh widget""" + pass + + def closing_plugin(self, cancelable=False): + """Perform actions before parent main window is closed""" + return True + + def apply_plugin_settings(self, options): + """Apply configuration file's plugin settings""" + pass + + def show(self): + """Show the breakpoints dockwidget""" + if self.dockwidget and not self.ismaximized: + self.dockwidget.setVisible(True) + self.dockwidget.setFocus() + self.dockwidget.raise_() diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder_breakpoints/__init__.py spyder-3.0.2+dfsg1/spyder_breakpoints/__init__.py --- spyder-2.3.8+dfsg1/spyder_breakpoints/__init__.py 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder_breakpoints/__init__.py 2016-10-25 02:05:23.000000000 +0200 @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- + +#============================================================================== +# The following statement is required to register this 3rd party plugin: +#============================================================================== +from .breakpoints import Breakpoints as PLUGIN_CLASS Binärdateien spyder-2.3.8+dfsg1/spyder_breakpoints/locale/es/LC_MESSAGES/breakpoints.mo und spyder-3.0.2+dfsg1/spyder_breakpoints/locale/es/LC_MESSAGES/breakpoints.mo sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder_breakpoints/locale/fr/LC_MESSAGES/breakpoints.mo und spyder-3.0.2+dfsg1/spyder_breakpoints/locale/fr/LC_MESSAGES/breakpoints.mo sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder_breakpoints/locale/pt_BR/LC_MESSAGES/breakpoints.mo und spyder-3.0.2+dfsg1/spyder_breakpoints/locale/pt_BR/LC_MESSAGES/breakpoints.mo sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder_breakpoints/locale/ru/LC_MESSAGES/breakpoints.mo und spyder-3.0.2+dfsg1/spyder_breakpoints/locale/ru/LC_MESSAGES/breakpoints.mo sind verschieden. diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder_breakpoints/widgets/breakpointsgui.py spyder-3.0.2+dfsg1/spyder_breakpoints/widgets/breakpointsgui.py --- spyder-2.3.8+dfsg1/spyder_breakpoints/widgets/breakpointsgui.py 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder_breakpoints/widgets/breakpointsgui.py 2016-10-25 02:05:23.000000000 +0200 @@ -0,0 +1,272 @@ +# -*- coding: utf-8 -*- +# +# Copyright © Spyder Project Contributors +# based loosley on pylintgui.py by Pierre Raybaut +# Licensed under the terms of the MIT License +# (see spyder/__init__.py for details) + +"""Breakpoint widget""" + +# pylint: disable=C0103 +# pylint: disable=R0903 +# pylint: disable=R0911 +# pylint: disable=R0201 + +# Standard library imports +import os.path as osp +import sys + +# Third party imports +from qtpy import API +from qtpy.compat import to_qvariant +from qtpy.QtCore import (QAbstractTableModel, QModelIndex, QTextCodec, Qt, + Signal) +from qtpy.QtWidgets import (QItemDelegate, QMenu, QTableView, QVBoxLayout, + QWidget) + +# Local imports +from spyder.config.base import get_translation +from spyder.config.main import CONF +from spyder.utils.qthelpers import add_actions, create_action + +# This is needed for testing this module as a stand alone script +try: + _ = get_translation("breakpoints", "spyder_breakpoints") +except KeyError as error: + import gettext + _ = gettext.gettext + + +locale_codec = QTextCodec.codecForLocale() + + +class BreakpointTableModel(QAbstractTableModel): + """ + Table model for breakpoints dictionary + + """ + def __init__(self, parent, data): + QAbstractTableModel.__init__(self, parent) + if data is None: + data = {} + self._data = None + self.breakpoints = None + self.set_data(data) + + def set_data(self, data): + """Set model data""" + self._data = data + keys = list(data.keys()) + self.breakpoints = [] + for key in keys: + bp_list = data[key] + if bp_list: + for item in data[key]: + self.breakpoints.append((key, item[0], item[1], "")) + self.reset() + + def rowCount(self, qindex=QModelIndex()): + """Array row number""" + return len(self.breakpoints) + + def columnCount(self, qindex=QModelIndex()): + """Array column count""" + return 4 + + def sort(self, column, order=Qt.DescendingOrder): + """Overriding sort method""" + if column == 0: + self.breakpoints.sort( + key=lambda breakpoint: breakpoint[1]) + self.breakpoints.sort( + key=lambda breakpoint: osp.basename(breakpoint[0])) + elif column == 1: + pass + elif column == 2: + pass + elif column == 3: + pass + self.reset() + + def headerData(self, section, orientation, role=Qt.DisplayRole): + """Overriding method headerData""" + if role != Qt.DisplayRole: + return to_qvariant() + i_column = int(section) + if orientation == Qt.Horizontal: + headers = (_("File"), _("Line"), _("Condition"), "") + return to_qvariant( headers[i_column] ) + else: + return to_qvariant() + + def get_value(self, index): + """Return current value""" + return self.breakpoints[index.row()][index.column()] + + def data(self, index, role=Qt.DisplayRole): + """Return data at table index""" + if not index.isValid(): + return to_qvariant() + if role == Qt.DisplayRole: + if index.column() == 0: + value = osp.basename(self.get_value(index)) + return to_qvariant(value) + else: + value = self.get_value(index) + return to_qvariant(value) + elif role == Qt.TextAlignmentRole: + return to_qvariant(int(Qt.AlignLeft|Qt.AlignVCenter)) + elif role == Qt.ToolTipRole: + if index.column() == 0: + value = self.get_value(index) + return to_qvariant(value) + else: + return to_qvariant() + + def reset(self): + self.beginResetModel() + self.endResetModel() + + +class BreakpointDelegate(QItemDelegate): + def __init__(self, parent=None): + QItemDelegate.__init__(self, parent) + + +class BreakpointTableView(QTableView): + edit_goto = Signal(str, int, str) + clear_breakpoint = Signal(str, int) + clear_all_breakpoints = Signal() + set_or_edit_conditional_breakpoint = Signal() + + def __init__(self, parent, data): + QTableView.__init__(self, parent) + self.model = BreakpointTableModel(self, data) + self.setModel(self.model) + self.delegate = BreakpointDelegate(self) + self.setItemDelegate(self.delegate) + + self.setup_table() + + def setup_table(self): + """Setup table""" + self.horizontalHeader().setStretchLastSection(True) + self.adjust_columns() + self.columnAt(0) + # Sorting columns + self.setSortingEnabled(False) + self.sortByColumn(0, Qt.DescendingOrder) + + def adjust_columns(self): + """Resize three first columns to contents""" + for col in range(3): + self.resizeColumnToContents(col) + + def mouseDoubleClickEvent(self, event): + """Reimplement Qt method""" + index_clicked = self.indexAt(event.pos()) + if self.model.breakpoints: + filename = self.model.breakpoints[index_clicked.row()][0] + line_number_str = self.model.breakpoints[index_clicked.row()][1] + self.edit_goto.emit(filename, int(line_number_str), '') + if index_clicked.column()==2: + self.set_or_edit_conditional_breakpoint.emit() + + def contextMenuEvent(self, event): + index_clicked = self.indexAt(event.pos()) + actions = [] + self.popup_menu = QMenu(self) + clear_all_breakpoints_action = create_action(self, + _("Clear breakpoints in all files"), + triggered=lambda: self.clear_all_breakpoints.emit()) + actions.append(clear_all_breakpoints_action) + if self.model.breakpoints: + filename = self.model.breakpoints[index_clicked.row()][0] + lineno = int(self.model.breakpoints[index_clicked.row()][1]) + # QAction.triggered works differently for PySide and PyQt + if not API == 'pyside': + clear_slot = lambda _checked, filename=filename, lineno=lineno: \ + self.clear_breakpoint.emit(filename, lineno) + edit_slot = lambda _checked, filename=filename, lineno=lineno: \ + (self.edit_goto.emit(filename, lineno, ''), + self.set_or_edit_conditional_breakpoint.emit()) + else: + clear_slot = lambda filename=filename, lineno=lineno: \ + self.clear_breakpoint.emit(filename, lineno) + edit_slot = lambda filename=filename, lineno=lineno: \ + (self.edit_goto.emit(filename, lineno, ''), + self.set_or_edit_conditional_breakpoint.emit()) + + clear_breakpoint_action = create_action(self, + _("Clear this breakpoint"), + triggered=clear_slot) + actions.insert(0,clear_breakpoint_action) + + edit_breakpoint_action = create_action(self, + _("Edit this breakpoint"), + triggered=edit_slot) + actions.append(edit_breakpoint_action) + add_actions(self.popup_menu, actions) + self.popup_menu.popup(event.globalPos()) + event.accept() + + +class BreakpointWidget(QWidget): + """ + Breakpoint widget + """ + VERSION = '1.0.0' + clear_all_breakpoints = Signal() + set_or_edit_conditional_breakpoint = Signal() + clear_breakpoint = Signal(str, int) + edit_goto = Signal(str, int, str) + + def __init__(self, parent): + QWidget.__init__(self, parent) + + self.setWindowTitle("Breakpoints") + self.dictwidget = BreakpointTableView(self, + self._load_all_breakpoints()) + layout = QVBoxLayout() + layout.addWidget(self.dictwidget) + self.setLayout(layout) + self.dictwidget.clear_all_breakpoints.connect( + lambda: self.clear_all_breakpoints.emit()) + self.dictwidget.clear_breakpoint.connect( + lambda s1, lino: self.clear_breakpoint.emit(s1, lino)) + self.dictwidget.edit_goto.connect( + lambda s1, lino, s2: self.edit_goto.emit(s1, lino, s2)) + self.dictwidget.set_or_edit_conditional_breakpoint.connect( + lambda: self.set_or_edit_conditional_breakpoint.emit()) + + def _load_all_breakpoints(self): + bp_dict = CONF.get('run', 'breakpoints', {}) + for filename in list(bp_dict.keys()): + if not osp.isfile(filename): + bp_dict.pop(filename) + return bp_dict + + def get_data(self): + pass + + def set_data(self): + bp_dict = self._load_all_breakpoints() + self.dictwidget.model.set_data(bp_dict) + self.dictwidget.adjust_columns() + self.dictwidget.sortByColumn(0, Qt.DescendingOrder) + + +#============================================================================== +# Tests +#============================================================================== +def test(): + """Run breakpoint widget test""" + from spyder.utils.qthelpers import qapplication + app = qapplication() + widget = BreakpointWidget(None) + widget.show() + sys.exit(app.exec_()) + + +if __name__ == '__main__': + test() diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder.egg-info/dependency_links.txt spyder-3.0.2+dfsg1/spyder.egg-info/dependency_links.txt --- spyder-2.3.8+dfsg1/spyder.egg-info/dependency_links.txt 2015-11-27 14:35:50.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder.egg-info/dependency_links.txt 1970-01-01 01:00:00.000000000 +0100 @@ -1 +0,0 @@ - diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder.egg-info/PKG-INFO spyder-3.0.2+dfsg1/spyder.egg-info/PKG-INFO --- spyder-2.3.8+dfsg1/spyder.egg-info/PKG-INFO 2015-11-27 14:35:50.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder.egg-info/PKG-INFO 1970-01-01 01:00:00.000000000 +0100 @@ -1,31 +0,0 @@ -Metadata-Version: 1.1 -Name: spyder -Version: 2.3.8 -Summary: Scientific PYthon Development EnviRonment -Home-page: https://github.com/spyder-ide/spyder -Author: Pierre Raybaut -Author-email: UNKNOWN -License: MIT -Download-URL: https://github.com/spyder-ide/spyder/files/spyder-2.3.8.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 PyQt4 or - PySide application: source code editor with syntax highlighting and - code introspection/analysis features, NumPy array editor, dictionary - editor, Python console, etc. -Keywords: PyQt4 PySide editor shell console widgets IDE -Platform: any -Classifier: License :: OSI Approved :: MIT License -Classifier: Operating System :: MacOS -Classifier: Operating System :: Microsoft :: Windows -Classifier: Operating System :: OS Independent -Classifier: Operating System :: POSIX -Classifier: Operating System :: Unix -Classifier: Programming Language :: Python :: 2.7 -Classifier: Programming Language :: Python :: 3 -Classifier: Development Status :: 5 - Production/Stable -Classifier: Topic :: Scientific/Engineering -Classifier: Topic :: Software Development :: Widget Sets -Requires: rope (>=0.9.2) -Requires: sphinx (>=0.6.0) -Requires: PyQt4 (>=4.4) diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder.egg-info/SOURCES.txt spyder-3.0.2+dfsg1/spyder.egg-info/SOURCES.txt --- spyder-2.3.8+dfsg1/spyder.egg-info/SOURCES.txt 2015-11-27 14:35:54.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder.egg-info/SOURCES.txt 1970-01-01 01:00:00.000000000 +0100 @@ -1,692 +0,0 @@ -LICENSE -MANIFEST.in -README.md -bootstrap.py -setup.py -app_example/create_exe.py -app_example/example.pyw -doc/conf.py -doc/console.rst -doc/debugging.rst -doc/editor.rst -doc/explorer.rst -doc/findinfiles.rst -doc/historylog.rst -doc/index.rst -doc/inspector.rst -doc/installation.rst -doc/internalconsole.rst -doc/ipythonconsole.rst -doc/lightmode.rst -doc/onlinehelp.rst -doc/options.rst -doc/overview.rst -doc/projectexplorer.rst -doc/pylint.rst -doc/spyder_bbg.png -doc/variableexplorer.rst -doc/_static/favicon.ico -doc/images/arrayeditor.png -doc/images/console.png -doc/images/dicteditor.png -doc/images/editor1.png -doc/images/editor2.png -doc/images/editor3.png -doc/images/explorer.png -doc/images/explorer_menu1.png -doc/images/explorer_menu2.png -doc/images/findinfiles.png -doc/images/git_install_dialog.png -doc/images/historylog.png -doc/images/inspector_plain.png -doc/images/inspector_rich.png -doc/images/inspector_source.png -doc/images/internalconsole.png -doc/images/ipythonconsole.png -doc/images/ipythonconsolemenu.png -doc/images/ipythonkernelconnect.png -doc/images/lightmode.png -doc/images/listeditor.png -doc/images/onlinehelp.png -doc/images/projectexplorer.png -doc/images/projectexplorer2.png -doc/images/pylint.png -doc/images/texteditor.png -doc/images/variableexplorer-imshow.png -doc/images/variableexplorer-plot.png -doc/images/variableexplorer1.png -doc/images/variableexplorer2.png -img_src/spyder.ico -img_src/spyder.png -img_src/spyder3.png -img_src/spyder_light.ico -scripts/spyder -scripts/spyder.bat -scripts/spyder.desktop -scripts/spyder3 -scripts/spyder3.desktop -scripts/spyder_win_post_install.py -spyder.egg-info/PKG-INFO -spyder.egg-info/SOURCES.txt -spyder.egg-info/dependency_links.txt -spyder.egg-info/top_level.txt -spyderlib/__init__.py -spyderlib/baseconfig.py -spyderlib/cli_options.py -spyderlib/config.py -spyderlib/dependencies.py -spyderlib/guiconfig.py -spyderlib/interpreter.py -spyderlib/ipythonconfig.py -spyderlib/mac_stylesheet.qss -spyderlib/otherplugins.py -spyderlib/pil_patch.py -spyderlib/py3compat.py -spyderlib/pygments_patch.py -spyderlib/pyplot.py -spyderlib/requirements.py -spyderlib/rope_patch.py -spyderlib/scientific_startup.py -spyderlib/spyder.py -spyderlib/start_app.py -spyderlib/userconfig.py -spyderlib/defaults/Readme.txt -spyderlib/defaults/defaults-2.4.0.ini -spyderlib/defaults/defaults-3.0.0.ini -spyderlib/images/advanced.png -spyderlib/images/arredit.png -spyderlib/images/arrow.png -spyderlib/images/bold.png -spyderlib/images/browser.png -spyderlib/images/chevron-left.png -spyderlib/images/chevron-right.png -spyderlib/images/dictedit.png -spyderlib/images/font.png -spyderlib/images/genprefs.png -spyderlib/images/inspector.png -spyderlib/images/italic.png -spyderlib/images/matplotlib.png -spyderlib/images/none.png -spyderlib/images/not_found.png -spyderlib/images/options.svg -spyderlib/images/pythonpath_mgr.png -spyderlib/images/pythonxy.png -spyderlib/images/qt.png -spyderlib/images/qtassistant.png -spyderlib/images/qtdesigner.png -spyderlib/images/qtlinguist.png -spyderlib/images/scipy.png -spyderlib/images/set_workdir.png -spyderlib/images/splash.png -spyderlib/images/spyder.svg -spyderlib/images/spyder_light.svg -spyderlib/images/upper_lower.png -spyderlib/images/vcs_browse.png -spyderlib/images/vcs_commit.png -spyderlib/images/vitables.png -spyderlib/images/whole_words.png -spyderlib/images/win_env.png -spyderlib/images/winpython.svg -spyderlib/images/actions/1downarrow.png -spyderlib/images/actions/1uparrow.png -spyderlib/images/actions/2downarrow.png -spyderlib/images/actions/2uparrow.png -spyderlib/images/actions/arrow-continue.png -spyderlib/images/actions/arrow-step-in.png -spyderlib/images/actions/arrow-step-out.png -spyderlib/images/actions/arrow-step-over.png -spyderlib/images/actions/auto_reload.png -spyderlib/images/actions/browse_tab.png -spyderlib/images/actions/check.png -spyderlib/images/actions/cmdprompt.png -spyderlib/images/actions/collapse.png -spyderlib/images/actions/collapse_selection.png -spyderlib/images/actions/configure.png -spyderlib/images/actions/copywop.png -spyderlib/images/actions/delete.png -spyderlib/images/actions/edit.png -spyderlib/images/actions/edit24.png -spyderlib/images/actions/edit_add.png -spyderlib/images/actions/edit_remove.png -spyderlib/images/actions/editcopy.png -spyderlib/images/actions/editcut.png -spyderlib/images/actions/editdelete.png -spyderlib/images/actions/editpaste.png -spyderlib/images/actions/eraser.png -spyderlib/images/actions/exit.png -spyderlib/images/actions/expand.png -spyderlib/images/actions/expand_selection.png -spyderlib/images/actions/filter.png -spyderlib/images/actions/find.png -spyderlib/images/actions/findf.png -spyderlib/images/actions/findnext.png -spyderlib/images/actions/findprevious.png -spyderlib/images/actions/folder_new.png -spyderlib/images/actions/hide.png -spyderlib/images/actions/hist.png -spyderlib/images/actions/home.png -spyderlib/images/actions/imshow.png -spyderlib/images/actions/insert.png -spyderlib/images/actions/lock.png -spyderlib/images/actions/lock_open.png -spyderlib/images/actions/magnifier.png -spyderlib/images/actions/maximize.png -spyderlib/images/actions/next.png -spyderlib/images/actions/options_less.png -spyderlib/images/actions/options_more.png -spyderlib/images/actions/plot.png -spyderlib/images/actions/previous.png -spyderlib/images/actions/redo.png -spyderlib/images/actions/reload.png -spyderlib/images/actions/rename.png -spyderlib/images/actions/replace.png -spyderlib/images/actions/restore.png -spyderlib/images/actions/show.png -spyderlib/images/actions/special_paste.png -spyderlib/images/actions/stop.png -spyderlib/images/actions/stop_debug.png -spyderlib/images/actions/synchronize.png -spyderlib/images/actions/tooloptions.png -spyderlib/images/actions/undo.png -spyderlib/images/actions/unmaximize.png -spyderlib/images/actions/up.png -spyderlib/images/actions/window_fullscreen.png -spyderlib/images/actions/window_nofullscreen.png -spyderlib/images/actions/zoom_in.png -spyderlib/images/actions/zoom_out.png -spyderlib/images/console/clear.png -spyderlib/images/console/cmdprompt_t.png -spyderlib/images/console/console.png -spyderlib/images/console/environ.png -spyderlib/images/console/history.png -spyderlib/images/console/history24.png -spyderlib/images/console/ipython_console.png -spyderlib/images/console/ipython_console_t.png -spyderlib/images/console/kill.png -spyderlib/images/console/loading_sprites.png -spyderlib/images/console/prompt.png -spyderlib/images/console/python.png -spyderlib/images/console/python_t.png -spyderlib/images/console/restart.png -spyderlib/images/console/run_small.png -spyderlib/images/console/syspath.png -spyderlib/images/console/terminated.png -spyderlib/images/editor/blockcomment.png -spyderlib/images/editor/breakpoint_big.png -spyderlib/images/editor/breakpoint_cond_big.png -spyderlib/images/editor/breakpoint_cond_small.png -spyderlib/images/editor/breakpoint_small.png -spyderlib/images/editor/bug.png -spyderlib/images/editor/cell.png -spyderlib/images/editor/class.png -spyderlib/images/editor/close_panel.png -spyderlib/images/editor/comment.png -spyderlib/images/editor/convention.png -spyderlib/images/editor/debug.png -spyderlib/images/editor/error.png -spyderlib/images/editor/file.png -spyderlib/images/editor/filelist.png -spyderlib/images/editor/fromcursor.png -spyderlib/images/editor/function.png -spyderlib/images/editor/gotoline.png -spyderlib/images/editor/highlight.png -spyderlib/images/editor/horsplit.png -spyderlib/images/editor/indent.png -spyderlib/images/editor/last_edit_location.png -spyderlib/images/editor/method.png -spyderlib/images/editor/newwindow.png -spyderlib/images/editor/next_cursor.png -spyderlib/images/editor/next_wng.png -spyderlib/images/editor/outline_explorer.png -spyderlib/images/editor/outline_explorer_vis.png -spyderlib/images/editor/prev_cursor.png -spyderlib/images/editor/prev_wng.png -spyderlib/images/editor/private1.png -spyderlib/images/editor/private2.png -spyderlib/images/editor/refactor.png -spyderlib/images/editor/run.png -spyderlib/images/editor/run_again.png -spyderlib/images/editor/run_cell.png -spyderlib/images/editor/run_cell_advance.png -spyderlib/images/editor/run_selection.png -spyderlib/images/editor/run_settings.png -spyderlib/images/editor/select.png -spyderlib/images/editor/selectall.png -spyderlib/images/editor/todo.png -spyderlib/images/editor/todo_list.png -spyderlib/images/editor/uncomment.png -spyderlib/images/editor/unindent.png -spyderlib/images/editor/versplit.png -spyderlib/images/editor/warning.png -spyderlib/images/editor/wng_list.png -spyderlib/images/file/fileclose.png -spyderlib/images/file/filecloseall.png -spyderlib/images/file/fileimport.png -spyderlib/images/file/filenew.png -spyderlib/images/file/fileopen.png -spyderlib/images/file/filesave.png -spyderlib/images/file/filesaveas.png -spyderlib/images/file/print.png -spyderlib/images/file/save_all.png -spyderlib/images/filetypes/bat.png -spyderlib/images/filetypes/bmp.png -spyderlib/images/filetypes/c.png -spyderlib/images/filetypes/cc.png -spyderlib/images/filetypes/cfg.png -spyderlib/images/filetypes/chm.png -spyderlib/images/filetypes/cl.png -spyderlib/images/filetypes/cmd.png -spyderlib/images/filetypes/cpp.png -spyderlib/images/filetypes/css.png -spyderlib/images/filetypes/cxx.png -spyderlib/images/filetypes/diff.png -spyderlib/images/filetypes/doc.png -spyderlib/images/filetypes/enaml.png -spyderlib/images/filetypes/exe.png -spyderlib/images/filetypes/f.png -spyderlib/images/filetypes/f77.png -spyderlib/images/filetypes/f90.png -spyderlib/images/filetypes/gif.png -spyderlib/images/filetypes/h.png -spyderlib/images/filetypes/hh.png -spyderlib/images/filetypes/hpp.png -spyderlib/images/filetypes/htm.png -spyderlib/images/filetypes/html.png -spyderlib/images/filetypes/hxx.png -spyderlib/images/filetypes/inf.png -spyderlib/images/filetypes/ini.png -spyderlib/images/filetypes/jl.png -spyderlib/images/filetypes/jpeg.png -spyderlib/images/filetypes/jpg.png -spyderlib/images/filetypes/js.png -spyderlib/images/filetypes/log.png -spyderlib/images/filetypes/nsh.png -spyderlib/images/filetypes/nsi.png -spyderlib/images/filetypes/nt.png -spyderlib/images/filetypes/patch.png -spyderlib/images/filetypes/pdf.png -spyderlib/images/filetypes/png.png -spyderlib/images/filetypes/po.png -spyderlib/images/filetypes/pot.png -spyderlib/images/filetypes/pps.png -spyderlib/images/filetypes/properties.png -spyderlib/images/filetypes/ps.png -spyderlib/images/filetypes/pxd.png -spyderlib/images/filetypes/pxi.png -spyderlib/images/filetypes/py.png -spyderlib/images/filetypes/pyc.png -spyderlib/images/filetypes/pyw.png -spyderlib/images/filetypes/pyx.png -spyderlib/images/filetypes/rar.png -spyderlib/images/filetypes/readme.png -spyderlib/images/filetypes/reg.png -spyderlib/images/filetypes/rej.png -spyderlib/images/filetypes/session.png -spyderlib/images/filetypes/tar.png -spyderlib/images/filetypes/tex.png -spyderlib/images/filetypes/tgz.png -spyderlib/images/filetypes/tif.png -spyderlib/images/filetypes/tiff.png -spyderlib/images/filetypes/ts.png -spyderlib/images/filetypes/txt.png -spyderlib/images/filetypes/ui.png -spyderlib/images/filetypes/xls.png -spyderlib/images/filetypes/xml.png -spyderlib/images/filetypes/zip.png -spyderlib/images/projects/add_to_path.png -spyderlib/images/projects/folder.png -spyderlib/images/projects/package.png -spyderlib/images/projects/pp_folder.png -spyderlib/images/projects/pp_package.png -spyderlib/images/projects/pp_project.png -spyderlib/images/projects/project.png -spyderlib/images/projects/project_closed.png -spyderlib/images/projects/pydev.png -spyderlib/images/projects/pythonpath.png -spyderlib/images/projects/remove_from_path.png -spyderlib/images/projects/show_all.png -spyderlib/locale/spyderlib.pot -spyderlib/locale/es/LC_MESSAGES/spyderlib.mo -spyderlib/locale/es/LC_MESSAGES/spyderlib.po -spyderlib/locale/fr/LC_MESSAGES/spyderlib.mo -spyderlib/locale/fr/LC_MESSAGES/spyderlib.po -spyderlib/plugins/__init__.py -spyderlib/plugins/configdialog.py -spyderlib/plugins/console.py -spyderlib/plugins/editor.py -spyderlib/plugins/explorer.py -spyderlib/plugins/externalconsole.py -spyderlib/plugins/findinfiles.py -spyderlib/plugins/history.py -spyderlib/plugins/inspector.py -spyderlib/plugins/ipythonconsole.py -spyderlib/plugins/onlinehelp.py -spyderlib/plugins/outlineexplorer.py -spyderlib/plugins/projectexplorer.py -spyderlib/plugins/runconfig.py -spyderlib/plugins/shortcuts.py -spyderlib/plugins/variableexplorer.py -spyderlib/plugins/workingdirectory.py -spyderlib/qt/QtCore.py -spyderlib/qt/QtGui.py -spyderlib/qt/QtSvg.py -spyderlib/qt/QtWebKit.py -spyderlib/qt/__init__.py -spyderlib/qt/compat.py -spyderlib/utils/__init__.py -spyderlib/utils/bsdsocket.py -spyderlib/utils/codeanalysis.py -spyderlib/utils/debug.py -spyderlib/utils/dochelpers.py -spyderlib/utils/encoding.py -spyderlib/utils/environ.py -spyderlib/utils/iofuncs.py -spyderlib/utils/misc.py -spyderlib/utils/programs.py -spyderlib/utils/qthelpers.py -spyderlib/utils/sourcecode.py -spyderlib/utils/system.py -spyderlib/utils/vcs.py -spyderlib/utils/windows.py -spyderlib/utils/external/__init__.py -spyderlib/utils/external/lockfile.py -spyderlib/utils/external/path.py -spyderlib/utils/external/pickleshare.py -spyderlib/utils/inspector/__init__.py -spyderlib/utils/inspector/conf.py -spyderlib/utils/inspector/sphinxify.py -spyderlib/utils/inspector/tutorial.rst -spyderlib/utils/inspector/js/collapse_sections.js -spyderlib/utils/inspector/js/copy_button.js -spyderlib/utils/inspector/js/fix_image_paths.js -spyderlib/utils/inspector/js/jquery.js -spyderlib/utils/inspector/js/math_config.js -spyderlib/utils/inspector/js/move_outline.js -spyderlib/utils/inspector/js/utils.js -spyderlib/utils/inspector/js/mathjax/MathJax.js -spyderlib/utils/inspector/js/mathjax/config/TeX-AMS-MML_HTMLorMML-full.js -spyderlib/utils/inspector/js/mathjax/config/TeX-AMS-MML_HTMLorMML.js -spyderlib/utils/inspector/js/mathjax/config/TeX-AMS-MML_SVG-full.js -spyderlib/utils/inspector/js/mathjax/config/TeX-AMS-MML_SVG.js -spyderlib/utils/inspector/js/mathjax/config/TeX-MML-AM_HTMLorMML-full.js -spyderlib/utils/inspector/js/mathjax/config/TeX-MML-AM_HTMLorMML.js -spyderlib/utils/inspector/js/mathjax/config/default.js -spyderlib/utils/inspector/js/mathjax/config/local/local.js -spyderlib/utils/inspector/js/mathjax/extensions/FontWarnings.js -spyderlib/utils/inspector/js/mathjax/extensions/MathEvents.js -spyderlib/utils/inspector/js/mathjax/extensions/MathMenu.js -spyderlib/utils/inspector/js/mathjax/extensions/MathZoom.js -spyderlib/utils/inspector/js/mathjax/extensions/asciimath2jax.js -spyderlib/utils/inspector/js/mathjax/extensions/jsMath2jax.js -spyderlib/utils/inspector/js/mathjax/extensions/mml2jax.js -spyderlib/utils/inspector/js/mathjax/extensions/tex2jax.js -spyderlib/utils/inspector/js/mathjax/extensions/toMathML.js -spyderlib/utils/inspector/js/mathjax/extensions/v1.0-warning.js -spyderlib/utils/inspector/js/mathjax/extensions/HTML-CSS/handle-floats.js -spyderlib/utils/inspector/js/mathjax/extensions/TeX/AMSmath.js -spyderlib/utils/inspector/js/mathjax/extensions/TeX/AMSsymbols.js -spyderlib/utils/inspector/js/mathjax/extensions/TeX/HTML.js -spyderlib/utils/inspector/js/mathjax/extensions/TeX/action.js -spyderlib/utils/inspector/js/mathjax/extensions/TeX/autobold.js -spyderlib/utils/inspector/js/mathjax/extensions/TeX/autoload-all.js -spyderlib/utils/inspector/js/mathjax/extensions/TeX/bbox.js -spyderlib/utils/inspector/js/mathjax/extensions/TeX/begingroup.js -spyderlib/utils/inspector/js/mathjax/extensions/TeX/boldsymbol.js -spyderlib/utils/inspector/js/mathjax/extensions/TeX/cancel.js -spyderlib/utils/inspector/js/mathjax/extensions/TeX/color.js -spyderlib/utils/inspector/js/mathjax/extensions/TeX/enclose.js -spyderlib/utils/inspector/js/mathjax/extensions/TeX/extpfeil.js -spyderlib/utils/inspector/js/mathjax/extensions/TeX/mathchoice.js -spyderlib/utils/inspector/js/mathjax/extensions/TeX/mhchem.js -spyderlib/utils/inspector/js/mathjax/extensions/TeX/newcommand.js -spyderlib/utils/inspector/js/mathjax/extensions/TeX/noErrors.js -spyderlib/utils/inspector/js/mathjax/extensions/TeX/noUndefined.js -spyderlib/utils/inspector/js/mathjax/extensions/TeX/unicode.js -spyderlib/utils/inspector/js/mathjax/extensions/TeX/verb.js -spyderlib/utils/inspector/js/mathjax/images/CloseX-31.png -spyderlib/utils/inspector/js/mathjax/images/MenuArrow-15.png -spyderlib/utils/inspector/js/mathjax/jax/element/mml/jax.js -spyderlib/utils/inspector/js/mathjax/jax/element/mml/optable/Arrows.js -spyderlib/utils/inspector/js/mathjax/jax/element/mml/optable/BasicLatin.js -spyderlib/utils/inspector/js/mathjax/jax/element/mml/optable/CombDiacritMarks.js -spyderlib/utils/inspector/js/mathjax/jax/element/mml/optable/CombDiactForSymbols.js -spyderlib/utils/inspector/js/mathjax/jax/element/mml/optable/Dingbats.js -spyderlib/utils/inspector/js/mathjax/jax/element/mml/optable/GeneralPunctuation.js -spyderlib/utils/inspector/js/mathjax/jax/element/mml/optable/GeometricShapes.js -spyderlib/utils/inspector/js/mathjax/jax/element/mml/optable/GreekAndCoptic.js -spyderlib/utils/inspector/js/mathjax/jax/element/mml/optable/Latin1Supplement.js -spyderlib/utils/inspector/js/mathjax/jax/element/mml/optable/LetterlikeSymbols.js -spyderlib/utils/inspector/js/mathjax/jax/element/mml/optable/MathOperators.js -spyderlib/utils/inspector/js/mathjax/jax/element/mml/optable/MiscMathSymbolsA.js -spyderlib/utils/inspector/js/mathjax/jax/element/mml/optable/MiscMathSymbolsB.js -spyderlib/utils/inspector/js/mathjax/jax/element/mml/optable/MiscSymbolsAndArrows.js -spyderlib/utils/inspector/js/mathjax/jax/element/mml/optable/MiscTechnical.js -spyderlib/utils/inspector/js/mathjax/jax/element/mml/optable/SpacingModLetters.js -spyderlib/utils/inspector/js/mathjax/jax/element/mml/optable/SuppMathOperators.js -spyderlib/utils/inspector/js/mathjax/jax/element/mml/optable/SupplementalArrowsA.js -spyderlib/utils/inspector/js/mathjax/jax/element/mml/optable/SupplementalArrowsB.js -spyderlib/utils/inspector/js/mathjax/jax/input/AsciiMath/config.js -spyderlib/utils/inspector/js/mathjax/jax/input/AsciiMath/jax.js -spyderlib/utils/inspector/js/mathjax/jax/input/MathML/config.js -spyderlib/utils/inspector/js/mathjax/jax/input/MathML/jax.js -spyderlib/utils/inspector/js/mathjax/jax/input/MathML/entities/a.js -spyderlib/utils/inspector/js/mathjax/jax/input/MathML/entities/b.js -spyderlib/utils/inspector/js/mathjax/jax/input/MathML/entities/c.js -spyderlib/utils/inspector/js/mathjax/jax/input/MathML/entities/d.js -spyderlib/utils/inspector/js/mathjax/jax/input/MathML/entities/e.js -spyderlib/utils/inspector/js/mathjax/jax/input/MathML/entities/f.js -spyderlib/utils/inspector/js/mathjax/jax/input/MathML/entities/fr.js -spyderlib/utils/inspector/js/mathjax/jax/input/MathML/entities/g.js -spyderlib/utils/inspector/js/mathjax/jax/input/MathML/entities/h.js -spyderlib/utils/inspector/js/mathjax/jax/input/MathML/entities/i.js -spyderlib/utils/inspector/js/mathjax/jax/input/MathML/entities/j.js -spyderlib/utils/inspector/js/mathjax/jax/input/MathML/entities/k.js -spyderlib/utils/inspector/js/mathjax/jax/input/MathML/entities/l.js -spyderlib/utils/inspector/js/mathjax/jax/input/MathML/entities/m.js -spyderlib/utils/inspector/js/mathjax/jax/input/MathML/entities/n.js -spyderlib/utils/inspector/js/mathjax/jax/input/MathML/entities/o.js -spyderlib/utils/inspector/js/mathjax/jax/input/MathML/entities/opf.js -spyderlib/utils/inspector/js/mathjax/jax/input/MathML/entities/p.js -spyderlib/utils/inspector/js/mathjax/jax/input/MathML/entities/q.js -spyderlib/utils/inspector/js/mathjax/jax/input/MathML/entities/r.js -spyderlib/utils/inspector/js/mathjax/jax/input/MathML/entities/s.js -spyderlib/utils/inspector/js/mathjax/jax/input/MathML/entities/scr.js -spyderlib/utils/inspector/js/mathjax/jax/input/MathML/entities/t.js -spyderlib/utils/inspector/js/mathjax/jax/input/MathML/entities/u.js -spyderlib/utils/inspector/js/mathjax/jax/input/MathML/entities/v.js -spyderlib/utils/inspector/js/mathjax/jax/input/MathML/entities/w.js -spyderlib/utils/inspector/js/mathjax/jax/input/MathML/entities/x.js -spyderlib/utils/inspector/js/mathjax/jax/input/MathML/entities/y.js -spyderlib/utils/inspector/js/mathjax/jax/input/MathML/entities/z.js -spyderlib/utils/inspector/js/mathjax/jax/input/TeX/config.js -spyderlib/utils/inspector/js/mathjax/jax/input/TeX/jax.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/config.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/jax.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/autoload/annotation-xml.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/autoload/maction.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/autoload/menclose.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/autoload/mglyph.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/autoload/mmultiscripts.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/autoload/ms.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/autoload/mtable.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/autoload/multiline.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/fontdata-extra.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/fontdata.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/AMS/Regular/Arrows.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/AMS/Regular/BoxDrawing.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/AMS/Regular/CombDiacritMarks.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/AMS/Regular/Dingbats.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/AMS/Regular/EnclosedAlphanum.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/AMS/Regular/GeneralPunctuation.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/AMS/Regular/GeometricShapes.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/AMS/Regular/GreekAndCoptic.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/AMS/Regular/Latin1Supplement.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/AMS/Regular/LatinExtendedA.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/AMS/Regular/LetterlikeSymbols.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/AMS/Regular/Main.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/AMS/Regular/MathOperators.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/AMS/Regular/MiscMathSymbolsB.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/AMS/Regular/MiscSymbols.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/AMS/Regular/MiscTechnical.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/AMS/Regular/PUA.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/AMS/Regular/SpacingModLetters.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/AMS/Regular/SuppMathOperators.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/Caligraphic/Bold/Main.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/Caligraphic/Regular/Main.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/Fraktur/Bold/BasicLatin.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/Fraktur/Bold/Main.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/Fraktur/Bold/Other.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/Fraktur/Bold/PUA.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/Fraktur/Regular/BasicLatin.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/Fraktur/Regular/Main.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/Fraktur/Regular/Other.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/Fraktur/Regular/PUA.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/Main/Bold/Arrows.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/Main/Bold/BasicLatin.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/Main/Bold/CombDiacritMarks.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/Main/Bold/CombDiactForSymbols.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/Main/Bold/GeneralPunctuation.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/Main/Bold/GeometricShapes.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/Main/Bold/GreekAndCoptic.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/Main/Bold/Latin1Supplement.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/Main/Bold/LatinExtendedA.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/Main/Bold/LatinExtendedB.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/Main/Bold/LetterlikeSymbols.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/Main/Bold/Main.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/Main/Bold/MathOperators.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/Main/Bold/MiscMathSymbolsA.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/Main/Bold/MiscSymbols.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/Main/Bold/MiscTechnical.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/Main/Bold/SpacingModLetters.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/Main/Bold/SuppMathOperators.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/Main/Bold/SupplementalArrowsA.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/Main/Italic/BasicLatin.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/Main/Italic/CombDiacritMarks.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/Main/Italic/GeneralPunctuation.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/Main/Italic/GreekAndCoptic.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/Main/Italic/LatinExtendedA.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/Main/Italic/LatinExtendedB.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/Main/Italic/LetterlikeSymbols.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/Main/Italic/Main.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/Main/Italic/MathOperators.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/Main/Regular/BasicLatin.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/Main/Regular/CombDiacritMarks.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/Main/Regular/GeometricShapes.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/Main/Regular/GreekAndCoptic.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/Main/Regular/LatinExtendedA.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/Main/Regular/LatinExtendedB.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/Main/Regular/LetterlikeSymbols.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/Main/Regular/Main.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/Main/Regular/MathOperators.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/Main/Regular/MiscSymbols.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/Main/Regular/SpacingModLetters.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/Main/Regular/SuppMathOperators.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/Math/BoldItalic/Main.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/Math/Italic/Main.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/SansSerif/Bold/BasicLatin.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/SansSerif/Bold/CombDiacritMarks.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/SansSerif/Bold/Main.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/SansSerif/Bold/Other.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/SansSerif/Italic/BasicLatin.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/SansSerif/Italic/CombDiacritMarks.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/SansSerif/Italic/Main.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/SansSerif/Italic/Other.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/SansSerif/Regular/BasicLatin.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/SansSerif/Regular/CombDiacritMarks.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/SansSerif/Regular/Main.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/SansSerif/Regular/Other.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/Script/Regular/BasicLatin.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/Script/Regular/Main.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/Size1/Regular/Main.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/Size2/Regular/Main.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/Size3/Regular/Main.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/Size4/Regular/Main.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/Typewriter/Regular/BasicLatin.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/Typewriter/Regular/CombDiacritMarks.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/Typewriter/Regular/Main.js -spyderlib/utils/inspector/js/mathjax/jax/output/SVG/fonts/TeX/Typewriter/Regular/Other.js -spyderlib/utils/inspector/static/css/default.css -spyderlib/utils/inspector/static/css/pygments.css -spyderlib/utils/inspector/static/images/collapse_expand.png -spyderlib/utils/inspector/static/images/debug-continue.png -spyderlib/utils/inspector/static/images/debug-step-in.png -spyderlib/utils/inspector/static/images/debug-step-out.png -spyderlib/utils/inspector/static/images/debug-step-over.png -spyderlib/utils/inspector/static/images/spyder-hello-docstring.png -spyderlib/utils/inspector/static/images/spyder-nice-docstring-rendering.png -spyderlib/utils/inspector/static/images/spyder-sympy-example.png -spyderlib/utils/inspector/templates/layout.html -spyderlib/utils/inspector/templates/usage.html -spyderlib/utils/inspector/templates/warning.html -spyderlib/utils/introspection/__init__.py -spyderlib/utils/introspection/fallback_plugin.py -spyderlib/utils/introspection/jedi_plugin.py -spyderlib/utils/introspection/module_completion.py -spyderlib/utils/introspection/plugin_manager.py -spyderlib/utils/introspection/rope_plugin.py -spyderlib/utils/ipython/templates/blank.html -spyderlib/utils/ipython/templates/kernel_error.html -spyderlib/utils/ipython/templates/loading.html -spyderlib/widgets/__init__.py -spyderlib/widgets/arrayeditor.py -spyderlib/widgets/browser.py -spyderlib/widgets/calltip.py -spyderlib/widgets/colors.py -spyderlib/widgets/comboboxes.py -spyderlib/widgets/dataframeeditor.py -spyderlib/widgets/dependencies.py -spyderlib/widgets/dicteditor.py -spyderlib/widgets/dicteditorutils.py -spyderlib/widgets/editor.py -spyderlib/widgets/editortools.py -spyderlib/widgets/explorer.py -spyderlib/widgets/findinfiles.py -spyderlib/widgets/findreplace.py -spyderlib/widgets/formlayout.py -spyderlib/widgets/importwizard.py -spyderlib/widgets/internalshell.py -spyderlib/widgets/ipython.py -spyderlib/widgets/mixins.py -spyderlib/widgets/objecteditor.py -spyderlib/widgets/onecolumntree.py -spyderlib/widgets/pathmanager.py -spyderlib/widgets/projectexplorer.py -spyderlib/widgets/pydocgui.py -spyderlib/widgets/shell.py -spyderlib/widgets/status.py -spyderlib/widgets/tabs.py -spyderlib/widgets/texteditor.py -spyderlib/widgets/externalshell/__init__.py -spyderlib/widgets/externalshell/baseshell.py -spyderlib/widgets/externalshell/inputhooks.py -spyderlib/widgets/externalshell/introspection.py -spyderlib/widgets/externalshell/monitor.py -spyderlib/widgets/externalshell/namespacebrowser.py -spyderlib/widgets/externalshell/osx_app_site.py -spyderlib/widgets/externalshell/pythonshell.py -spyderlib/widgets/externalshell/sitecustomize.py -spyderlib/widgets/externalshell/start_ipython_kernel.py -spyderlib/widgets/externalshell/systemshell.py -spyderlib/widgets/sourcecode/__init__.py -spyderlib/widgets/sourcecode/base.py -spyderlib/widgets/sourcecode/codeeditor.py -spyderlib/widgets/sourcecode/syntaxhighlighters.py -spyderlib/widgets/sourcecode/terminal.py -spyderplugins/__init__.py -spyderplugins/io_dicom.py -spyderplugins/io_hdf5.py -spyderplugins/p_breakpoints.py -spyderplugins/p_profiler.py -spyderplugins/p_pylint.py -spyderplugins/images/profiler.png -spyderplugins/images/pylint.png -spyderplugins/locale/es/LC_MESSAGES/p_breakpoints.mo -spyderplugins/locale/es/LC_MESSAGES/p_profiler.mo -spyderplugins/locale/es/LC_MESSAGES/p_pylint.mo -spyderplugins/locale/fr/LC_MESSAGES/p_breakpoints.mo -spyderplugins/locale/fr/LC_MESSAGES/p_profiler.mo -spyderplugins/locale/fr/LC_MESSAGES/p_pylint.mo -spyderplugins/widgets/__init__.py -spyderplugins/widgets/breakpointsgui.py -spyderplugins/widgets/profilergui.py -spyderplugins/widgets/pylintgui.py \ Kein Zeilenumbruch am Dateiende. diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder.egg-info/top_level.txt spyder-3.0.2+dfsg1/spyder.egg-info/top_level.txt --- spyder-2.3.8+dfsg1/spyder.egg-info/top_level.txt 2015-11-27 14:35:50.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder.egg-info/top_level.txt 1970-01-01 01:00:00.000000000 +0100 @@ -1,2 +0,0 @@ -spyderlib -spyderplugins diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder_io_dcm/dcm.py spyder-3.0.2+dfsg1/spyder_io_dcm/dcm.py --- spyder-2.3.8+dfsg1/spyder_io_dcm/dcm.py 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder_io_dcm/dcm.py 2016-10-25 02:05:23.000000000 +0200 @@ -0,0 +1,26 @@ +# -*- coding:utf-8 -*- +"""Example of I/O plugin for loading DICOM files""" + +import os.path as osp + +try: + try: + # pydicom 0.9 + import dicom as dicomio + except ImportError: + # pydicom 1.0 + from pydicom import dicomio + def load_dicom(filename): + try: + name = osp.splitext(osp.basename(filename))[0] + try: + data = dicomio.read_file(filename, force=True) + except TypeError: + data = dicomio.read_file(filename) + arr = data.pixel_array + return {name: arr}, None + except Exception as error: + return None, str(error) +except ImportError: + load_dicom = None + diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder_io_dcm/__init__.py spyder-3.0.2+dfsg1/spyder_io_dcm/__init__.py --- spyder-2.3.8+dfsg1/spyder_io_dcm/__init__.py 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder_io_dcm/__init__.py 2016-10-25 02:05:23.000000000 +0200 @@ -0,0 +1,9 @@ +# ============================================================================= +# The following statements are required to register this I/O plugin: +# ============================================================================= +from .dcm import load_dicom + +FORMAT_NAME = "DICOM images" +FORMAT_EXT = ".dcm" +FORMAT_LOAD = load_dicom +FORMAT_SAVE = None \ Kein Zeilenumbruch am Dateiende. diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder_io_hdf5/hdf5.py spyder-3.0.2+dfsg1/spyder_io_hdf5/hdf5.py --- spyder-2.3.8+dfsg1/spyder_io_hdf5/hdf5.py 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder_io_hdf5/hdf5.py 2016-10-25 02:05:23.000000000 +0200 @@ -0,0 +1,78 @@ +# -*- coding:utf-8 -*- +# +# Copyright © Spyder Project Contributors +# Licensed under the terms of the MIT License +# (see spyder/__init__.py for details) + +"""I/O plugin for loading/saving HDF5 files + +Note that this is a fairly dumb implementation which reads the whole HDF5 file into +Spyder's variable explorer. Since HDF5 files are designed for storing very large +data-sets, it may be much better to work directly with the HDF5 objects, thus keeping +the data on disk. Nonetheless, this plugin gives quick and dirty but convenient +access to HDF5 files. + +There is no support for creating files with compression, chunking etc, although +these can be read without problem. + +All datatypes to be saved must be convertible to a numpy array, otherwise an exception +will be raised. + +Data attributes are currently ignored. + +When reading an HDF5 file with sub-groups, groups in the HDF5 file will +correspond to dictionaries with the same layout. However, when saving +data, dictionaries are not turned into HDF5 groups. + +TODO: Look for the pytables library if h5py is not found?? +TODO: Check issues with valid python names vs valid h5f5 names +""" + +from __future__ import print_function + +try: + # Do not import h5py here because it will try to import IPython, + # and this is freezing the Spyder GUI + import imp + imp.find_module('h5py') + import numpy as np + + def load_hdf5(filename): + import h5py + def get_group(group): + contents = {} + for name, obj in list(group.items()): + if isinstance(obj, h5py.Dataset): + contents[name] = np.array(obj) + elif isinstance(obj, h5py.Group): + # it is a group, so call self recursively + contents[name] = get_group(obj) + # other objects such as links are ignored + return contents + + try: + f = h5py.File(filename, 'r') + contents = get_group(f) + f.close() + return contents, None + except Exception as error: + return None, str(error) + + def save_hdf5(data, filename): + import h5py + try: + f = h5py.File(filename, 'w') + for key, value in list(data.items()): + f[key] = np.array(value) + f.close() + except Exception as error: + return str(error) +except ImportError: + load_hdf5 = None + save_hdf5 = None + + +if __name__ == "__main__": + data = {'a' : [1, 2, 3, 4], 'b' : 4.5} + print(save_hdf5(data, "test.h5")) + print(load_hdf5("test.h5")) diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder_io_hdf5/__init__.py spyder-3.0.2+dfsg1/spyder_io_hdf5/__init__.py --- spyder-2.3.8+dfsg1/spyder_io_hdf5/__init__.py 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder_io_hdf5/__init__.py 2016-10-25 02:05:23.000000000 +0200 @@ -0,0 +1,9 @@ +# ============================================================================= +# The following statements are required to register this I/O plugin: +# ============================================================================= +from .hdf5 import load_hdf5, save_hdf5 + +FORMAT_NAME = "HDF5" +FORMAT_EXT = ".h5" +FORMAT_LOAD = load_hdf5 +FORMAT_SAVE = save_hdf5 Binärdateien spyder-2.3.8+dfsg1/spyderplugins/images/profiler.png und spyder-3.0.2+dfsg1/spyderplugins/images/profiler.png sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyderplugins/images/pylint.png und spyder-3.0.2+dfsg1/spyderplugins/images/pylint.png sind verschieden. diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyderplugins/io_dicom.py spyder-3.0.2+dfsg1/spyderplugins/io_dicom.py --- spyder-2.3.8+dfsg1/spyderplugins/io_dicom.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyderplugins/io_dicom.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,23 +0,0 @@ -# -*- coding:utf-8 -*- -"""Example of I/O plugin for loading DICOM files""" - -import os.path as osp - -try: - import dicom - def load_dicom(filename): - try: - name = osp.splitext(osp.basename(filename))[0] - return {name: dicom.ReadFile(filename).PixelArray}, None - except Exception as error: - return None, str(error) -except ImportError: - load_dicom = None - -#=============================================================================== -# The following statements are required to register this I/O plugin: -#=============================================================================== -FORMAT_NAME = "DICOM images" -FORMAT_EXT = ".dcm" -FORMAT_LOAD = load_dicom -FORMAT_SAVE = None diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyderplugins/io_hdf5.py spyder-3.0.2+dfsg1/spyderplugins/io_hdf5.py --- spyder-2.3.8+dfsg1/spyderplugins/io_hdf5.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyderplugins/io_hdf5.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,85 +0,0 @@ -# -*- coding:utf-8 -*- -# -# Copyright © 2011 David Anthony Powell -# Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) - -"""I/O plugin for loading/saving HDF5 files - -Note that this is a fairly dumb implementation which reads the whole HDF5 file into -Spyder's variable explorer. Since HDF5 files are designed for storing very large -data-sets, it may be much better to work directly with the HDF5 objects, thus keeping -the data on disk. Nonetheless, this plugin gives quick and dirty but convenient -access to HDF5 files. - -There is no support for creating files with compression, chunking etc, although -these can be read without problem. - -All datatypes to be saved must be convertible to a numpy array, otherwise an exception -will be raised. - -Data attributes are currently ignored. - -When reading an HDF5 file with sub-groups, groups in the HDF5 file will -correspond to dictionaries with the same layout. However, when saving -data, dictionaries are not turned into HDF5 groups. - -TODO: Look for the pytables library if h5py is not found?? -TODO: Check issues with valid python names vs valid h5f5 names -""" - -from __future__ import print_function - -try: - # Do not import h5py here because it will try to import IPython, - # and this is freezing the Spyder GUI - import imp - imp.find_module('h5py') - import numpy as np - - def load_hdf5(filename): - import h5py - def get_group(group): - contents = {} - for name, obj in list(group.items()): - if isinstance(obj, h5py.Dataset): - contents[name] = np.array(obj) - elif isinstance(obj, h5py.Group): - # it is a group, so call self recursively - contents[name] = get_group(obj) - # other objects such as links are ignored - return contents - - try: - f = h5py.File(filename, 'r') - contents = get_group(f) - f.close() - return contents, None - except Exception as error: - return None, str(error) - - def save_hdf5(data, filename): - import h5py - try: - f = h5py.File(filename, 'w') - for key, value in list(data.items()): - f[key] = np.array(value) - f.close() - except Exception as error: - return str(error) -except ImportError: - load_hdf5 = None - save_hdf5 = None - -#=============================================================================== -# The following statements are required to register this I/O plugin: -#=============================================================================== -FORMAT_NAME = "HDF5" -FORMAT_EXT = ".h5" -FORMAT_LOAD = load_hdf5 -FORMAT_SAVE = save_hdf5 - -if __name__ == "__main__": - data = {'a' : [1, 2, 3, 4], 'b' : 4.5} - print(save_hdf5(data, "test.h5")) - print(load_hdf5("test.h5")) Binärdateien spyder-2.3.8+dfsg1/spyderplugins/locale/es/LC_MESSAGES/p_breakpoints.mo und spyder-3.0.2+dfsg1/spyderplugins/locale/es/LC_MESSAGES/p_breakpoints.mo sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyderplugins/locale/es/LC_MESSAGES/p_profiler.mo und spyder-3.0.2+dfsg1/spyderplugins/locale/es/LC_MESSAGES/p_profiler.mo sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyderplugins/locale/es/LC_MESSAGES/p_pylint.mo und spyder-3.0.2+dfsg1/spyderplugins/locale/es/LC_MESSAGES/p_pylint.mo sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyderplugins/locale/fr/LC_MESSAGES/p_breakpoints.mo und spyder-3.0.2+dfsg1/spyderplugins/locale/fr/LC_MESSAGES/p_breakpoints.mo sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyderplugins/locale/fr/LC_MESSAGES/p_profiler.mo und spyder-3.0.2+dfsg1/spyderplugins/locale/fr/LC_MESSAGES/p_profiler.mo sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyderplugins/locale/fr/LC_MESSAGES/p_pylint.mo und spyder-3.0.2+dfsg1/spyderplugins/locale/fr/LC_MESSAGES/p_pylint.mo sind verschieden. diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyderplugins/p_breakpoints.py spyder-3.0.2+dfsg1/spyderplugins/p_breakpoints.py --- spyder-2.3.8+dfsg1/spyderplugins/p_breakpoints.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyderplugins/p_breakpoints.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,128 +0,0 @@ -# -*- coding:utf-8 -*- -# -# Copyright © 2012 Jed Ludlow -# Based loosely on p_pylint.py by Pierre Raybaut -# Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) - -"""Breakpoint Plugin""" - -# pylint: disable=C0103 -# pylint: disable=R0903 -# pylint: disable=R0911 -# pylint: disable=R0201 - -from spyderlib.qt.QtCore import SIGNAL - -# Local imports -from spyderlib.baseconfig import get_translation -_ = get_translation("p_breakpoints", dirname="spyderplugins") -from spyderlib.utils.qthelpers import get_icon, create_action -from spyderlib.plugins import SpyderPluginMixin -from spyderplugins.widgets.breakpointsgui import BreakpointWidget -from spyderlib.py3compat import to_text_string, is_text_string - - -class Breakpoints(BreakpointWidget, SpyderPluginMixin): - """Breakpoint list""" - CONF_SECTION = 'breakpoints' -# CONFIGWIDGET_CLASS = BreakpointConfigPage - def __init__(self, parent=None): - BreakpointWidget.__init__(self, parent=parent) - SpyderPluginMixin.__init__(self, parent) - - # Initialize plugin - self.initialize_plugin() - self.set_data() - - #------ SpyderPluginWidget API -------------------------------------------- - def get_plugin_title(self): - """Return widget title""" - return _("Breakpoints") - - def get_plugin_icon(self): - """Return widget icon""" - return get_icon('bug.png') - - def get_focus_widget(self): - """ - Return the widget to give focus to when - this plugin's dockwidget is raised on top-level - """ - return self.dictwidget - - def get_plugin_actions(self): - """Return a list of actions related to plugin""" - return [] - - def on_first_registration(self): - """Action to be performed on first plugin registration""" - self.main.tabify_plugins(self.main.inspector, self) - - def register_plugin(self): - """Register plugin in Spyder's main window""" - self.connect(self, SIGNAL("edit_goto(QString,int,QString)"), - self.main.editor.load) - self.connect(self, SIGNAL('redirect_stdio(bool)'), - self.main.redirect_internalshell_stdio) - self.connect(self, SIGNAL('clear_all_breakpoints()'), - self.main.editor.clear_all_breakpoints) - self.connect(self, SIGNAL('clear_breakpoint(QString,int)'), - self.main.editor.clear_breakpoint) - self.connect(self, SIGNAL('set_or_edit_conditional_breakpoint()'), - self.main.editor.set_or_edit_conditional_breakpoint) - self.connect(self.main.editor, - SIGNAL("breakpoints_saved()"), - self.set_data) - - self.main.add_dockwidget(self) - - list_action = create_action(self, _("List breakpoints"), - triggered=self.show) - list_action.setEnabled(True) - - # A fancy way to insert the action into the Breakpoints menu under - # the assumption that Breakpoints is the first QMenu in the list. - for item in self.main.debug_menu_actions: - try: - menu_title = item.title() - except AttributeError: - pass - else: - # Depending on Qt API version, could get a QString or - # unicode from title() - if not is_text_string(menu_title): # string is a QString - menu_title = to_text_string(menu_title.toUtf8) - item.addAction(list_action) - # If we've reached this point it means we've located the - # first QMenu in the run_menu. Since there might be other - # QMenu entries in run_menu, we'll break so that the - # breakpoint action is only inserted once into the run_menu. - break - self.main.editor.pythonfile_dependent_actions += [list_action] - - def refresh_plugin(self): - """Refresh widget""" - pass - - def closing_plugin(self, cancelable=False): - """Perform actions before parent main window is closed""" - return True - - def apply_plugin_settings(self, options): - """Apply configuration file's plugin settings""" - pass - - def show(self): - """Show the breakpoints dockwidget""" - if self.dockwidget and not self.ismaximized: - self.dockwidget.setVisible(True) - self.dockwidget.setFocus() - self.dockwidget.raise_() - - -#============================================================================== -# The following statements are required to register this 3rd party plugin: -#============================================================================== -PLUGIN_CLASS = Breakpoints - diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyderplugins/p_profiler.py spyder-3.0.2+dfsg1/spyderplugins/p_profiler.py --- spyder-2.3.8+dfsg1/spyderplugins/p_profiler.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyderplugins/p_profiler.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,146 +0,0 @@ -# -*- coding:utf-8 -*- -# -# Copyright © 2011 Santiago Jaramillo -# based on p_pylint.py by Pierre Raybaut -# -# Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) - -"""Profiler Plugin""" - -from spyderlib.qt.QtGui import QVBoxLayout, QGroupBox, QLabel -from spyderlib.qt.QtCore import SIGNAL, Qt - -# Local imports -from spyderlib.baseconfig import get_translation -_ = get_translation("p_profiler", dirname="spyderplugins") -from spyderlib.utils.qthelpers import get_icon, create_action -from spyderlib.plugins import SpyderPluginMixin, PluginConfigPage, runconfig - -from spyderplugins.widgets.profilergui import (ProfilerWidget, - is_profiler_installed) - - -class ProfilerConfigPage(PluginConfigPage): - def setup_page(self): - results_group = QGroupBox(_("Results")) - results_label1 = QLabel(_("Profiler plugin results "\ - "(the output of python's profile/cProfile)\n" - "are stored here:")) - results_label1.setWordWrap(True) - - # Warning: do not try to regroup the following QLabel contents with - # widgets above -- this string was isolated here in a single QLabel - # on purpose: to fix Issue 863 - results_label2 = QLabel(ProfilerWidget.DATAPATH) - - results_label2.setTextInteractionFlags(Qt.TextSelectableByMouse) - results_label2.setWordWrap(True) - - results_layout = QVBoxLayout() - results_layout.addWidget(results_label1) - results_layout.addWidget(results_label2) - results_group.setLayout(results_layout) - - vlayout = QVBoxLayout() - vlayout.addWidget(results_group) - vlayout.addStretch(1) - self.setLayout(vlayout) - -class Profiler(ProfilerWidget, SpyderPluginMixin): - """Profiler (after python's profile and pstats)""" - CONF_SECTION = 'profiler' - CONFIGWIDGET_CLASS = ProfilerConfigPage - def __init__(self, parent=None): - ProfilerWidget.__init__(self, parent=parent, - max_entries=self.get_option('max_entries', 50)) - SpyderPluginMixin.__init__(self, parent) - - # Initialize plugin - self.initialize_plugin() - - #------ SpyderPluginWidget API --------------------------------------------- - def get_plugin_title(self): - """Return widget title""" - return _("Profiler") - - def get_plugin_icon(self): - """Return widget icon""" - return get_icon('profiler.png') - - def get_focus_widget(self): - """ - Return the widget to give focus to when - this plugin's dockwidget is raised on top-level - """ - return self.datatree - - def get_plugin_actions(self): - """Return a list of actions related to plugin""" - return [] - - def on_first_registration(self): - """Action to be performed on first plugin registration""" - self.main.tabify_plugins(self.main.inspector, self) - self.dockwidget.hide() - - def register_plugin(self): - """Register plugin in Spyder's main window""" - self.connect(self, SIGNAL("edit_goto(QString,int,QString)"), - self.main.editor.load) - self.connect(self, SIGNAL('redirect_stdio(bool)'), - self.main.redirect_internalshell_stdio) - self.main.add_dockwidget(self) - - profiler_act = create_action(self, _("Profile"), - icon=get_icon('profiler.png'), - triggered=self.run_profiler) - profiler_act.setEnabled(is_profiler_installed()) - self.register_shortcut(profiler_act, context="Profiler", - name="Run profiler") - - self.main.run_menu_actions += [profiler_act] - self.main.editor.pythonfile_dependent_actions += [profiler_act] - - def refresh_plugin(self): - """Refresh profiler widget""" - #self.remove_obsolete_items() # FIXME: not implemented yet - - def closing_plugin(self, cancelable=False): - """Perform actions before parent main window is closed""" - return True - - def apply_plugin_settings(self, options): - """Apply configuration file's plugin settings""" - # The history depth option will be applied at - # next Spyder startup, which is soon enough - pass - - #------ Public API --------------------------------------------------------- - def run_profiler(self): - """Run profiler""" - self.analyze(self.main.editor.get_current_filename()) - - def analyze(self, filename): - """Reimplement analyze method""" - if self.dockwidget and not self.ismaximized: - self.dockwidget.setVisible(True) - self.dockwidget.setFocus() - self.dockwidget.raise_() - pythonpath = self.main.get_spyder_pythonpath() - runconf = runconfig.get_run_configuration(filename) - wdir, args = None, None - if runconf is not None: - if runconf.wdir_enabled: - wdir = runconf.wdir - if runconf.args_enabled: - args = runconf.args - ProfilerWidget.analyze(self, filename, wdir=wdir, args=args, - pythonpath=pythonpath) - - -#=============================================================================== -# The following statements are required to register this 3rd party plugin: -#=============================================================================== -PLUGIN_CLASS = Profiler - diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyderplugins/p_pylint.py spyder-3.0.2+dfsg1/spyderplugins/p_pylint.py --- spyder-2.3.8+dfsg1/spyderplugins/p_pylint.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyderplugins/p_pylint.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,178 +0,0 @@ -# -*- coding:utf-8 -*- -# -# Copyright © 2009-2011 Pierre Raybaut -# Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) - -"""Pylint Code Analysis Plugin""" - -# pylint: disable=C0103 -# pylint: disable=R0903 -# pylint: disable=R0911 -# pylint: disable=R0201 - -from spyderlib.qt.QtGui import QInputDialog, QVBoxLayout, QGroupBox, QLabel -from spyderlib.qt.QtCore import SIGNAL, Qt - -# Local imports -from spyderlib.baseconfig import get_translation -_ = get_translation("p_pylint", dirname="spyderplugins") -from spyderlib.utils.qthelpers import get_icon, create_action -from spyderlib.plugins import SpyderPluginMixin, PluginConfigPage - -from spyderplugins.widgets.pylintgui import PylintWidget, PYLINT_PATH - - -class PylintConfigPage(PluginConfigPage): - def setup_page(self): - settings_group = QGroupBox(_("Settings")) - save_box = self.create_checkbox(_("Save file before analyzing it"), - 'save_before', default=True) - - hist_group = QGroupBox(_("History")) - hist_label1 = QLabel(_("The following option will be applied at next " - "startup.")) - hist_label1.setWordWrap(True) - hist_spin = self.create_spinbox(_("History: "), - _(" results"), 'max_entries', default=50, - min_=10, max_=1000000, step=10) - - results_group = QGroupBox(_("Results")) - results_label1 = QLabel(_("Results are stored here:")) - results_label1.setWordWrap(True) - - # Warning: do not try to regroup the following QLabel contents with - # widgets above -- this string was isolated here in a single QLabel - # on purpose: to fix Issue 863 - results_label2 = QLabel(PylintWidget.DATAPATH) - - results_label2.setTextInteractionFlags(Qt.TextSelectableByMouse) - results_label2.setWordWrap(True) - - settings_layout = QVBoxLayout() - settings_layout.addWidget(save_box) - settings_group.setLayout(settings_layout) - - hist_layout = QVBoxLayout() - hist_layout.addWidget(hist_label1) - hist_layout.addWidget(hist_spin) - hist_group.setLayout(hist_layout) - - results_layout = QVBoxLayout() - results_layout.addWidget(results_label1) - results_layout.addWidget(results_label2) - results_group.setLayout(results_layout) - - vlayout = QVBoxLayout() - vlayout.addWidget(settings_group) - vlayout.addWidget(hist_group) - vlayout.addWidget(results_group) - vlayout.addStretch(1) - self.setLayout(vlayout) - - -class Pylint(PylintWidget, SpyderPluginMixin): - """Python source code analysis based on pylint""" - CONF_SECTION = 'pylint' - CONFIGWIDGET_CLASS = PylintConfigPage - def __init__(self, parent=None): - PylintWidget.__init__(self, parent=parent, - max_entries=self.get_option('max_entries', 50)) - SpyderPluginMixin.__init__(self, parent) - - # Initialize plugin - self.initialize_plugin() - - #------ SpyderPluginWidget API -------------------------------------------- - def get_plugin_title(self): - """Return widget title""" - return _("Static code analysis") - - def get_plugin_icon(self): - """Return widget icon""" - return get_icon('pylint.png') - - def get_focus_widget(self): - """ - Return the widget to give focus to when - this plugin's dockwidget is raised on top-level - """ - return self.treewidget - - def get_plugin_actions(self): - """Return a list of actions related to plugin""" - # Font - history_action = create_action(self, _("History..."), - None, 'history.png', - _("Set history maximum entries"), - triggered=self.change_history_depth) - self.treewidget.common_actions += (None, history_action) - return [] - - def on_first_registration(self): - """Action to be performed on first plugin registration""" - self.main.tabify_plugins(self.main.inspector, self) - self.dockwidget.hide() - - def register_plugin(self): - """Register plugin in Spyder's main window""" - self.connect(self, SIGNAL("edit_goto(QString,int,QString)"), - self.main.editor.load) - self.connect(self, SIGNAL('redirect_stdio(bool)'), - self.main.redirect_internalshell_stdio) - self.main.add_dockwidget(self) - - pylint_act = create_action(self, _("Run static code analysis"), - triggered=self.run_pylint) - pylint_act.setEnabled(PYLINT_PATH is not None) - self.register_shortcut(pylint_act, context="Pylint", - name="Run analysis") - - self.main.source_menu_actions += [None, pylint_act] - self.main.editor.pythonfile_dependent_actions += [pylint_act] - - def refresh_plugin(self): - """Refresh pylint widget""" - self.remove_obsolete_items() - - def closing_plugin(self, cancelable=False): - """Perform actions before parent main window is closed""" - return True - - def apply_plugin_settings(self, options): - """Apply configuration file's plugin settings""" - # The history depth option will be applied at - # next Spyder startup, which is soon enough - pass - - #------ Public API -------------------------------------------------------- - def change_history_depth(self): - "Change history max entries""" - depth, valid = QInputDialog.getInteger(self, _('History'), - _('Maximum entries'), - self.get_option('max_entries'), - 10, 10000) - if valid: - self.set_option('max_entries', depth) - - def run_pylint(self): - """Run pylint code analysis""" - if self.get_option('save_before', True)\ - and not self.main.editor.save(): - return - self.analyze( self.main.editor.get_current_filename() ) - - def analyze(self, filename): - """Reimplement analyze method""" - if self.dockwidget and not self.ismaximized: - self.dockwidget.setVisible(True) - self.dockwidget.setFocus() - self.dockwidget.raise_() - PylintWidget.analyze(self, filename) - - -#============================================================================== -# The following statements are required to register this 3rd party plugin: -#============================================================================== -PLUGIN_CLASS = Pylint - diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyderplugins/widgets/breakpointsgui.py spyder-3.0.2+dfsg1/spyderplugins/widgets/breakpointsgui.py --- spyder-2.3.8+dfsg1/spyderplugins/widgets/breakpointsgui.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyderplugins/widgets/breakpointsgui.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,234 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright © 2012 Jed Ludlow -# based loosley on pylintgui.py by Pierre Raybaut -# Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) - -"""Breakpoint widget""" - -# pylint: disable=C0103 -# pylint: disable=R0903 -# pylint: disable=R0911 -# pylint: disable=R0201 - -from spyderlib.qt.QtGui import (QWidget, QTableView, QItemDelegate, - QVBoxLayout, QMenu) -from spyderlib.qt.QtCore import (Qt, SIGNAL, QTextCodec, - QModelIndex, QAbstractTableModel) -locale_codec = QTextCodec.codecForLocale() -from spyderlib.qt.compat import to_qvariant -import sys -import os.path as osp - -# Local imports -from spyderlib.baseconfig import get_translation -from spyderlib.config import CONF -from spyderlib.utils.qthelpers import create_action, add_actions - -_ = get_translation("p_breakpoints", dirname="spyderplugins") - -class BreakpointTableModel(QAbstractTableModel): - """ - Table model for breakpoints dictionary - - """ - def __init__(self, parent, data): - QAbstractTableModel.__init__(self, parent) - if data is None: - data = {} - self._data = None - self.breakpoints = None - self.set_data(data) - - def set_data(self, data): - """Set model data""" - self._data = data - keys = list(data.keys()) - self.breakpoints = [] - for key in keys: - bp_list = data[key] - if bp_list: - for item in data[key]: - self.breakpoints.append((key, item[0], item[1], "")) - self.reset() - - def rowCount(self, qindex=QModelIndex()): - """Array row number""" - return len(self.breakpoints) - - def columnCount(self, qindex=QModelIndex()): - """Array column count""" - return 4 - - def sort(self, column, order=Qt.DescendingOrder): - """Overriding sort method""" - if column == 0: - self.breakpoints.sort( - key=lambda breakpoint: breakpoint[1]) - self.breakpoints.sort( - key=lambda breakpoint: osp.basename(breakpoint[0])) - elif column == 1: - pass - elif column == 2: - pass - elif column == 3: - pass - self.reset() - - def headerData(self, section, orientation, role=Qt.DisplayRole): - """Overriding method headerData""" - if role != Qt.DisplayRole: - return to_qvariant() - i_column = int(section) - if orientation == Qt.Horizontal: - headers = (_("File"), _("Line"), _("Condition"), "") - return to_qvariant( headers[i_column] ) - else: - return to_qvariant() - - def get_value(self, index): - """Return current value""" - return self.breakpoints[index.row()][index.column()] - - def data(self, index, role=Qt.DisplayRole): - """Return data at table index""" - if not index.isValid(): - return to_qvariant() - if role == Qt.DisplayRole: - if index.column() == 0: - value = osp.basename(self.get_value(index)) - return to_qvariant(value) - else: - value = self.get_value(index) - return to_qvariant(value) - elif role == Qt.TextAlignmentRole: - return to_qvariant(int(Qt.AlignLeft|Qt.AlignVCenter)) - elif role == Qt.ToolTipRole: - if index.column() == 0: - value = self.get_value(index) - return to_qvariant(value) - else: - return to_qvariant() - -class BreakpointDelegate(QItemDelegate): - def __init__(self, parent=None): - QItemDelegate.__init__(self, parent) - -class BreakpointTableView(QTableView): - def __init__(self, parent, data): - QTableView.__init__(self, parent) - self.model = BreakpointTableModel(self, data) - self.setModel(self.model) - self.delegate = BreakpointDelegate(self) - self.setItemDelegate(self.delegate) - - self.setup_table() - - def setup_table(self): - """Setup table""" - self.horizontalHeader().setStretchLastSection(True) - self.adjust_columns() - self.columnAt(0) - # Sorting columns - self.setSortingEnabled(False) - self.sortByColumn(0, Qt.DescendingOrder) - - def adjust_columns(self): - """Resize three first columns to contents""" - for col in range(3): - self.resizeColumnToContents(col) - - def mouseDoubleClickEvent(self, event): - """Reimplement Qt method""" - index_clicked = self.indexAt(event.pos()) - if self.model.breakpoints: - filename = self.model.breakpoints[index_clicked.row()][0] - line_number_str = self.model.breakpoints[index_clicked.row()][1] - self.emit(SIGNAL("edit_goto(QString,int,QString)"), - filename, int(line_number_str), '') - if index_clicked.column()==2: - self.emit(SIGNAL("set_or_edit_conditional_breakpoint()")) - - def contextMenuEvent(self, event): - index_clicked = self.indexAt(event.pos()) - actions = [] - self.popup_menu = QMenu(self) - clear_all_breakpoints_action = create_action(self, - _("Clear breakpoints in all files"), - triggered=lambda: self.emit(SIGNAL('clear_all_breakpoints()'))) - actions.append(clear_all_breakpoints_action) - if self.model.breakpoints: - filename = self.model.breakpoints[index_clicked.row()][0] - lineno = int(self.model.breakpoints[index_clicked.row()][1]) - clear_breakpoint_action = create_action(self, - _("Clear this breakpoint"), - triggered=lambda filename=filename, lineno=lineno: \ - self.emit(SIGNAL('clear_breakpoint(QString,int)'), - filename, lineno)) - actions.insert(0,clear_breakpoint_action) - - edit_breakpoint_action = create_action(self, - _("Edit this breakpoint"), - triggered=lambda filename=filename, lineno=lineno: \ - (self.emit(SIGNAL('edit_goto(QString,int,QString)'), - filename, lineno, ''), - self.emit(SIGNAL("set_or_edit_conditional_breakpoint()"))) - ) - actions.append(edit_breakpoint_action) - add_actions(self.popup_menu, actions) - self.popup_menu.popup(event.globalPos()) - event.accept() - -class BreakpointWidget(QWidget): - """ - Breakpoint widget - """ - VERSION = '1.0.0' - - def __init__(self, parent): - QWidget.__init__(self, parent) - - self.setWindowTitle("Breakpoints") - self.dictwidget = BreakpointTableView(self, - self._load_all_breakpoints()) - layout = QVBoxLayout() - layout.addWidget(self.dictwidget) - self.setLayout(layout) - self.connect(self.dictwidget, SIGNAL('clear_all_breakpoints()'), - lambda: self.emit(SIGNAL('clear_all_breakpoints()'))) - self.connect(self.dictwidget, SIGNAL('clear_breakpoint(QString,int)'), - lambda s1, lino: self.emit( - SIGNAL('clear_breakpoint(QString,int)'), s1, lino)) - self.connect(self.dictwidget, SIGNAL("edit_goto(QString,int,QString)"), - lambda s1, lino, s2: self.emit( - SIGNAL("edit_goto(QString,int,QString)"), s1, lino, s2)) - self.connect(self.dictwidget, SIGNAL('set_or_edit_conditional_breakpoint()'), - lambda: self.emit(SIGNAL('set_or_edit_conditional_breakpoint()'))) - - def _load_all_breakpoints(self): - bp_dict = CONF.get('run', 'breakpoints', {}) - for filename in list(bp_dict.keys()): - if not osp.isfile(filename): - bp_dict.pop(filename) - return bp_dict - - def get_data(self): - pass - - def set_data(self): - bp_dict = self._load_all_breakpoints() - self.dictwidget.model.set_data(bp_dict) - self.dictwidget.adjust_columns() - self.dictwidget.sortByColumn(0, Qt.DescendingOrder) - -def test(): - """Run breakpoint widget test""" - from spyderlib.utils.qthelpers import qapplication - app = qapplication() - widget = BreakpointWidget(None) - widget.show() - sys.exit(app.exec_()) - -if __name__ == '__main__': - test() diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyderplugins/widgets/__init__.py spyder-3.0.2+dfsg1/spyderplugins/widgets/__init__.py --- spyder-2.3.8+dfsg1/spyderplugins/widgets/__init__.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyderplugins/widgets/__init__.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,15 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright © 2009-2010 Pierre Raybaut -# Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) - -""" -spyderlib.widgets -================= - -Widgets defined in this module may be used in any other Qt-based application - -They are also used in Spyder through the Plugin interface -(see spyderlib.plugins) -""" diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyderplugins/widgets/profilergui.py spyder-3.0.2+dfsg1/spyderplugins/widgets/profilergui.py --- spyder-2.3.8+dfsg1/spyderplugins/widgets/profilergui.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyderplugins/widgets/profilergui.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,543 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright © 2011 Santiago Jaramillo -# based on pylintgui.py by Pierre Raybaut -# -# Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) - -""" -Profiler widget - -See the official documentation on python profiling: -http://docs.python.org/library/profile.html - -Questions for Pierre and others: - - Where in the menu should profiler go? Run > Profile code ? -""" - -from __future__ import with_statement - -from spyderlib.qt.QtGui import (QHBoxLayout, QWidget, QMessageBox, QVBoxLayout, - QLabel, QTreeWidget, QTreeWidgetItem, - QApplication) -from spyderlib.qt.QtCore import SIGNAL, QProcess, QByteArray, Qt, QTextCodec -locale_codec = QTextCodec.codecForLocale() -from spyderlib.qt.compat import getopenfilename - -import sys -import os -import os.path as osp -import time - -# Local imports -from spyderlib.utils.qthelpers import (create_toolbutton, get_item_user_text, - set_item_user_text, get_icon) -from spyderlib.utils.programs import shell_split -from spyderlib.baseconfig import get_conf_path, get_translation -from spyderlib.widgets.texteditor import TextEditor -from spyderlib.widgets.comboboxes import PythonModulesComboBox -from spyderlib.widgets.externalshell import baseshell -from spyderlib.py3compat import to_text_string, getcwd -_ = get_translation("p_profiler", dirname="spyderplugins") - - -def is_profiler_installed(): - from spyderlib.utils.programs import is_module_installed - return is_module_installed('cProfile') and is_module_installed('pstats') - - -class ProfilerWidget(QWidget): - """ - Profiler widget - """ - DATAPATH = get_conf_path('profiler.results') - VERSION = '0.0.1' - - def __init__(self, parent, max_entries=100): - QWidget.__init__(self, parent) - - self.setWindowTitle("Profiler") - - self.output = None - self.error_output = None - - self._last_wdir = None - self._last_args = None - self._last_pythonpath = None - - self.filecombo = PythonModulesComboBox(self) - - self.start_button = create_toolbutton(self, icon=get_icon('run.png'), - text=_("Profile"), - tip=_("Run profiler"), - triggered=self.start, text_beside_icon=True) - self.stop_button = create_toolbutton(self, - icon=get_icon('stop.png'), - text=_("Stop"), - tip=_("Stop current profiling"), - text_beside_icon=True) - self.connect(self.filecombo, SIGNAL('valid(bool)'), - self.start_button.setEnabled) - #self.connect(self.filecombo, SIGNAL('valid(bool)'), self.show_data) - # FIXME: The combobox emits this signal on almost any event - # triggering show_data() too early, too often. - - browse_button = create_toolbutton(self, icon=get_icon('fileopen.png'), - tip=_('Select Python script'), - triggered=self.select_file) - - self.datelabel = QLabel() - - self.log_button = create_toolbutton(self, icon=get_icon('log.png'), - text=_("Output"), - text_beside_icon=True, - tip=_("Show program's output"), - triggered=self.show_log) - - self.datatree = ProfilerDataTree(self) - - self.collapse_button = create_toolbutton(self, - icon=get_icon('collapse.png'), - triggered=lambda dD=-1: - self.datatree.change_view(dD), - tip=_('Collapse one level up')) - self.expand_button = create_toolbutton(self, - icon=get_icon('expand.png'), - triggered=lambda dD=1: - self.datatree.change_view(dD), - tip=_('Expand one level down')) - - hlayout1 = QHBoxLayout() - hlayout1.addWidget(self.filecombo) - hlayout1.addWidget(browse_button) - hlayout1.addWidget(self.start_button) - hlayout1.addWidget(self.stop_button) - - hlayout2 = QHBoxLayout() - hlayout2.addWidget(self.collapse_button) - hlayout2.addWidget(self.expand_button) - hlayout2.addStretch() - hlayout2.addWidget(self.datelabel) - hlayout2.addStretch() - hlayout2.addWidget(self.log_button) - - layout = QVBoxLayout() - layout.addLayout(hlayout1) - layout.addLayout(hlayout2) - layout.addWidget(self.datatree) - self.setLayout(layout) - - self.process = None - self.set_running_state(False) - self.start_button.setEnabled(False) - - if not is_profiler_installed(): - # This should happen only on certain GNU/Linux distributions - # or when this a home-made Python build because the Python - # profilers are included in the Python standard library - for widget in (self.datatree, self.filecombo, - self.start_button, self.stop_button): - widget.setDisabled(True) - url = 'http://docs.python.org/library/profile.html' - text = '%s %s' % (_('Please install'), url, - _("the Python profiler modules")) - self.datelabel.setText(text) - else: - pass # self.show_data() - - def analyze(self, filename, wdir=None, args=None, pythonpath=None): - if not is_profiler_installed(): - return - self.kill_if_running() - #index, _data = self.get_data(filename) - index = None # FIXME: storing data is not implemented yet - if index is None: - self.filecombo.addItem(filename) - self.filecombo.setCurrentIndex(self.filecombo.count()-1) - else: - self.filecombo.setCurrentIndex(self.filecombo.findText(filename)) - self.filecombo.selected() - if self.filecombo.is_valid(): - if wdir is None: - wdir = osp.dirname(filename) - self.start(wdir, args, pythonpath) - - def select_file(self): - self.emit(SIGNAL('redirect_stdio(bool)'), False) - filename, _selfilter = getopenfilename(self, _("Select Python script"), - getcwd(), _("Python scripts")+" (*.py ; *.pyw)") - self.emit(SIGNAL('redirect_stdio(bool)'), False) - if filename: - self.analyze(filename) - - def show_log(self): - if self.output: - TextEditor(self.output, title=_("Profiler output"), - readonly=True, size=(700, 500)).exec_() - - def show_errorlog(self): - if self.error_output: - TextEditor(self.error_output, title=_("Profiler output"), - readonly=True, size=(700, 500)).exec_() - - def start(self, wdir=None, args=None, pythonpath=None): - filename = to_text_string(self.filecombo.currentText()) - if wdir is None: - wdir = self._last_wdir - if wdir is None: - wdir = osp.basename(filename) - if args is None: - args = self._last_args - if args is None: - args = [] - if pythonpath is None: - pythonpath = self._last_pythonpath - self._last_wdir = wdir - self._last_args = args - self._last_pythonpath = pythonpath - - self.datelabel.setText(_('Profiling, please wait...')) - - self.process = QProcess(self) - self.process.setProcessChannelMode(QProcess.SeparateChannels) - self.process.setWorkingDirectory(wdir) - self.connect(self.process, SIGNAL("readyReadStandardOutput()"), - self.read_output) - self.connect(self.process, SIGNAL("readyReadStandardError()"), - lambda: self.read_output(error=True)) - self.connect(self.process, - SIGNAL("finished(int,QProcess::ExitStatus)"), - self.finished) - self.connect(self.stop_button, SIGNAL("clicked()"), self.process.kill) - - if pythonpath is not None: - env = [to_text_string(_pth) - for _pth in self.process.systemEnvironment()] - baseshell.add_pathlist_to_PYTHONPATH(env, pythonpath) - self.process.setEnvironment(env) - - self.output = '' - self.error_output = '' - - p_args = ['-m', 'cProfile', '-o', self.DATAPATH] - if os.name == 'nt': - # On Windows, one has to replace backslashes by slashes to avoid - # confusion with escape characters (otherwise, for example, '\t' - # will be interpreted as a tabulation): - p_args.append(osp.normpath(filename).replace(os.sep, '/')) - else: - p_args.append(filename) - if args: - p_args.extend(shell_split(args)) - executable = sys.executable - if executable.endswith("spyder.exe"): - # py2exe distribution - executable = "python.exe" - self.process.start(executable, p_args) - - running = self.process.waitForStarted() - self.set_running_state(running) - if not running: - QMessageBox.critical(self, _("Error"), - _("Process failed to start")) - - def set_running_state(self, state=True): - self.start_button.setEnabled(not state) - self.stop_button.setEnabled(state) - - def read_output(self, error=False): - if error: - self.process.setReadChannel(QProcess.StandardError) - else: - self.process.setReadChannel(QProcess.StandardOutput) - qba = QByteArray() - while self.process.bytesAvailable(): - if error: - qba += self.process.readAllStandardError() - else: - qba += self.process.readAllStandardOutput() - text = to_text_string( locale_codec.toUnicode(qba.data()) ) - if error: - self.error_output += text - else: - self.output += text - - def finished(self): - self.set_running_state(False) - self.show_errorlog() # If errors occurred, show them. - self.output = self.error_output + self.output - # FIXME: figure out if show_data should be called here or - # as a signal from the combobox - self.show_data(justanalyzed=True) - - def kill_if_running(self): - if self.process is not None: - if self.process.state() == QProcess.Running: - self.process.kill() - self.process.waitForFinished() - - def show_data(self, justanalyzed=False): - if not justanalyzed: - self.output = None - self.log_button.setEnabled(self.output is not None \ - and len(self.output) > 0) - self.kill_if_running() - filename = to_text_string(self.filecombo.currentText()) - if not filename: - return - - self.datatree.load_data(self.DATAPATH) - self.datelabel.setText(_('Sorting data, please wait...')) - QApplication.processEvents() - self.datatree.show_tree() - - text_style = "%s " - date_text = text_style % time.strftime("%d %b %Y %H:%M", - time.localtime()) - self.datelabel.setText(date_text) - - -class TreeWidgetItem( QTreeWidgetItem ): - def __init__(self, parent=None): - QTreeWidgetItem.__init__(self, parent) - - def __lt__(self, otherItem): - column = self.treeWidget().sortColumn() - try: - return float( self.text(column) ) > float( otherItem.text(column) ) - except ValueError: - return self.text(column) > otherItem.text(column) - - -class ProfilerDataTree(QTreeWidget): - """ - Convenience tree widget (with built-in model) - to store and view profiler data. - - The quantities calculated by the profiler are as follows - (from profile.Profile): - [0] = The number of times this function was called, not counting direct - or indirect recursion, - [1] = Number of times this function appears on the stack, minus one - [2] = Total time spent internal to this function - [3] = Cumulative time that this function was present on the stack. In - non-recursive functions, this is the total execution time from start - to finish of each invocation of a function, including time spent in - all subfunctions. - [4] = A dictionary indicating for each function name, the number of times - it was called by us. - """ - SEP = r"<[=]>" # separator between filename and linenumber - # (must be improbable as a filename to avoid splitting the filename itself) - def __init__(self, parent=None): - QTreeWidget.__init__(self, parent) - self.header_list = [_('Function/Module'), _('Total Time'), - _('Local Time'), _('Calls'), _('File:line')] - self.icon_list = {'module': 'python.png', - 'function': 'function.png', - 'builtin': 'python_t.png', - 'constructor': 'class.png'} - self.profdata = None # To be filled by self.load_data() - self.stats = None # To be filled by self.load_data() - self.item_depth = None - self.item_list = None - self.items_to_be_shown = None - self.current_view_depth = None - self.setColumnCount(len(self.header_list)) - self.setHeaderLabels(self.header_list) - self.initialize_view() - self.connect(self, SIGNAL('itemActivated(QTreeWidgetItem*,int)'), - self.item_activated) - self.connect(self, SIGNAL('itemExpanded(QTreeWidgetItem*)'), - self.item_expanded) - - def set_item_data(self, item, filename, line_number): - """Set tree item user data: filename (string) and line_number (int)""" - set_item_user_text(item, '%s%s%d' % (filename, self.SEP, line_number)) - - def get_item_data(self, item): - """Get tree item user data: (filename, line_number)""" - filename, line_number_str = get_item_user_text(item).split(self.SEP) - return filename, int(line_number_str) - - def initialize_view(self): - """Clean the tree and view parameters""" - self.clear() - self.item_depth = 0 # To be use for collapsing/expanding one level - self.item_list = [] # To be use for collapsing/expanding one level - self.items_to_be_shown = {} - self.current_view_depth = 0 - - def load_data(self, profdatafile): - """Load profiler data saved by profile/cProfile module""" - import pstats - self.profdata = pstats.Stats(profdatafile) - self.profdata.calc_callees() - self.stats = self.profdata.stats - - def find_root(self): - """Find a function without a caller""" - self.profdata.sort_stats("cumulative") - for func in self.profdata.fcn_list: - if ('~', 0, '') != func: - # This skips the profiler function at the top of the list - # it does only occur in Python 3 - return func - - def find_callees(self, parent): - """Find all functions called by (parent) function.""" - # FIXME: This implementation is very inneficient, because it - # traverses all the data to find children nodes (callees) - return self.profdata.all_callees[parent] - - def show_tree(self): - """Populate the tree with profiler data and display it.""" - self.initialize_view() # Clear before re-populating - self.setItemsExpandable(True) - self.setSortingEnabled(False) - rootkey = self.find_root() # This root contains profiler overhead - if rootkey: - self.populate_tree(self, self.find_callees(rootkey)) - self.resizeColumnToContents(0) - self.setSortingEnabled(True) - self.sortItems(1, Qt.DescendingOrder) # FIXME: hardcoded index - self.change_view(1) - - def function_info(self, functionKey): - """Returns processed information about the function's name and file.""" - node_type = 'function' - filename, line_number, function_name = functionKey - if function_name == '': - modulePath, moduleName = osp.split(filename) - node_type = 'module' - if moduleName == '__init__.py': - modulePath, moduleName = osp.split(modulePath) - function_name = '<' + moduleName + '>' - if not filename or filename == '~': - file_and_line = '(built-in)' - node_type = 'builtin' - else: - if function_name == '__init__': - node_type = 'constructor' - file_and_line = '%s : %d' % (filename, line_number) - return filename, line_number, function_name, file_and_line, node_type - - def populate_tree(self, parentItem, children_list): - """Recursive method to create each item (and associated data) in the tree.""" - for child_key in children_list: - self.item_depth += 1 - (filename, line_number, function_name, file_and_line, node_type - ) = self.function_info(child_key) - (primcalls, total_calls, loc_time, cum_time, callers - ) = self.stats[child_key] - child_item = TreeWidgetItem(parentItem) - self.item_list.append(child_item) - self.set_item_data(child_item, filename, line_number) - - # FIXME: indexes to data should be defined by a dictionary on init - child_item.setToolTip(0, 'Function or module name') - child_item.setData(0, Qt.DisplayRole, function_name) - child_item.setIcon(0, get_icon(self.icon_list[node_type])) - - child_item.setToolTip(1, _('Time in function '\ - '(including sub-functions)')) - #child_item.setData(1, Qt.DisplayRole, cum_time) - child_item.setData(1, Qt.DisplayRole, '%.3f' % cum_time) - child_item.setTextAlignment(1, Qt.AlignCenter) - - child_item.setToolTip(2, _('Local time in function '\ - '(not in sub-functions)')) - #child_item.setData(2, Qt.DisplayRole, loc_time) - child_item.setData(2, Qt.DisplayRole, '%.3f' % loc_time) - child_item.setTextAlignment(2, Qt.AlignCenter) - - child_item.setToolTip(3, _('Total number of calls '\ - '(including recursion)')) - child_item.setData(3, Qt.DisplayRole, total_calls) - child_item.setTextAlignment(3, Qt.AlignCenter) - - child_item.setToolTip(4, _('File:line '\ - 'where function is defined')) - child_item.setData(4, Qt.DisplayRole, file_and_line) - #child_item.setExpanded(True) - if self.is_recursive(child_item): - child_item.setData(4, Qt.DisplayRole, '(%s)' % _('recursion')) - child_item.setDisabled(True) - else: - callees = self.find_callees(child_key) - if self.item_depth < 3: - self.populate_tree(child_item, callees) - elif callees: - child_item.setChildIndicatorPolicy(child_item.ShowIndicator) - self.items_to_be_shown[id(child_item)] = callees - self.item_depth -= 1 - - def item_activated(self, item): - filename, line_number = self.get_item_data(item) - self.parent().emit(SIGNAL("edit_goto(QString,int,QString)"), - filename, line_number, '') - - def item_expanded(self, item): - if item.childCount() == 0 and id(item) in self.items_to_be_shown: - callees = self.items_to_be_shown[id(item)] - self.populate_tree(item, callees) - - def is_recursive(self, child_item): - """Returns True is a function is a descendant of itself.""" - ancestor = child_item.parent() - # FIXME: indexes to data should be defined by a dictionary on init - while ancestor: - if (child_item.data(0, Qt.DisplayRole - ) == ancestor.data(0, Qt.DisplayRole) and - child_item.data(4, Qt.DisplayRole - ) == ancestor.data(4, Qt.DisplayRole)): - return True - else: - ancestor = ancestor.parent() - return False - - def get_top_level_items(self): - """Iterate over top level items""" - return [self.topLevelItem(_i) for _i in range(self.topLevelItemCount())] - - def get_items(self, maxlevel): - """Return items (excluding top level items)""" - itemlist = [] - def add_to_itemlist(item, maxlevel, level=1): - level += 1 - for index in range(item.childCount()): - citem = item.child(index) - itemlist.append(citem) - if level <= maxlevel: - add_to_itemlist(citem, maxlevel, level) - for tlitem in self.get_top_level_items(): - itemlist.append(tlitem) - if maxlevel > 1: - add_to_itemlist(tlitem, maxlevel=maxlevel) - return itemlist - - def change_view(self, change_in_depth): - """Change the view depth by expand or collapsing all same-level nodes""" - self.current_view_depth += change_in_depth - if self.current_view_depth < 1: - self.current_view_depth = 1 - self.collapseAll() - for item in self.get_items(maxlevel=self.current_view_depth): - item.setExpanded(True) - - -def test(): - """Run widget test""" - from spyderlib.utils.qthelpers import qapplication - app = qapplication() - widget = ProfilerWidget(None) - widget.resize(800, 600) - widget.show() - #widget.analyze(__file__) - widget.analyze(osp.join(osp.dirname(__file__), os.pardir, os.pardir, - 'spyderlib/widgets', 'texteditor.py')) - sys.exit(app.exec_()) - -if __name__ == '__main__': - test() diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyderplugins/widgets/pylintgui.py spyder-3.0.2+dfsg1/spyderplugins/widgets/pylintgui.py --- spyder-2.3.8+dfsg1/spyderplugins/widgets/pylintgui.py 2015-11-27 14:29:56.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyderplugins/widgets/pylintgui.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,499 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright © 2009-2010 Pierre Raybaut -# Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) - -"""Pylint widget""" - -# pylint: disable=C0103 -# pylint: disable=R0903 -# pylint: disable=R0911 -# pylint: disable=R0201 - -from __future__ import with_statement, print_function - -from spyderlib.qt.QtGui import (QHBoxLayout, QWidget, QTreeWidgetItem, - QMessageBox, QVBoxLayout, QLabel) -from spyderlib.qt.QtCore import SIGNAL, QProcess, QByteArray, QTextCodec -locale_codec = QTextCodec.codecForLocale() -from spyderlib.qt.compat import getopenfilename - -import sys -import os -import os.path as osp -import time -import re -import subprocess - -# Local imports -from spyderlib import dependencies -from spyderlib.utils import programs -from spyderlib.utils.encoding import to_unicode_from_fs -from spyderlib.utils.qthelpers import get_icon, create_toolbutton -from spyderlib.baseconfig import get_conf_path, get_translation -from spyderlib.widgets.onecolumntree import OneColumnTree -from spyderlib.widgets.texteditor import TextEditor -from spyderlib.widgets.comboboxes import (PythonModulesComboBox, - is_module_or_package) -from spyderlib.py3compat import PY3, to_text_string, getcwd, pickle -_ = get_translation("p_pylint", dirname="spyderplugins") - - -PYLINT = 'pylint' -if PY3: - if programs.find_program('pylint3'): - PYLINT = 'pylint3' - elif programs.find_program('python3-pylint'): - PYLINT = 'python3-pylint' - -PYLINT_PATH = programs.find_program(PYLINT) - - -def get_pylint_version(): - """Return pylint version""" - global PYLINT_PATH - if PYLINT_PATH is None: - return - process = subprocess.Popen([PYLINT, '--version'], - stdout=subprocess.PIPE, stderr=subprocess.PIPE, - cwd=osp.dirname(PYLINT_PATH), - shell=True if os.name == 'nt' else False) - lines = to_unicode_from_fs(process.stdout.read()).splitlines() - if lines: - regex = '({0}*|pylint-script.py) ([0-9\.]*)'.format(PYLINT) - match = re.match(regex, lines[0]) - if match is not None: - return match.groups()[1] - - -PYLINT_REQVER = '>=0.25' -PYLINT_VER = get_pylint_version() -dependencies.add("pylint", _("Static code analysis"), - required_version=PYLINT_REQVER, installed_version=PYLINT_VER) - - -#TODO: display results on 3 columns instead of 1: msg_id, lineno, message -class ResultsTree(OneColumnTree): - def __init__(self, parent): - OneColumnTree.__init__(self, parent) - self.filename = None - self.results = None - self.data = None - self.set_title('') - - def activated(self, item): - """Double-click event""" - data = self.data.get(id(item)) - if data is not None: - fname, lineno = data - self.parent().emit(SIGNAL("edit_goto(QString,int,QString)"), - fname, lineno, '') - - def clicked(self, item): - """Click event""" - self.activated(item) - - def clear_results(self): - self.clear() - self.set_title('') - - def set_results(self, filename, results): - self.filename = filename - self.results = results - self.refresh() - - def refresh(self): - title = _('Results for ')+self.filename - self.set_title(title) - self.clear() - self.data = {} - # Populating tree - results = ((_('Convention'), - get_icon('convention.png'), self.results['C:']), - (_('Refactor'), - get_icon('refactor.png'), self.results['R:']), - (_('Warning'), - get_icon('warning.png'), self.results['W:']), - (_('Error'), - get_icon('error.png'), self.results['E:'])) - for title, icon, messages in results: - title += ' (%d message%s)' % (len(messages), - 's' if len(messages)>1 else '') - title_item = QTreeWidgetItem(self, [title], QTreeWidgetItem.Type) - title_item.setIcon(0, icon) - if not messages: - title_item.setDisabled(True) - modules = {} - for module, lineno, message, msg_id in messages: - basename = osp.splitext(osp.basename(self.filename))[0] - if not module.startswith(basename): - # Pylint bug - i_base = module.find(basename) - module = module[i_base:] - dirname = osp.dirname(self.filename) - if module.startswith('.') or module == basename: - modname = osp.join(dirname, module) - else: - modname = osp.join(dirname, *module.split('.')) - if osp.isdir(modname): - modname = osp.join(modname, '__init__') - for ext in ('.py', '.pyw'): - if osp.isfile(modname+ext): - modname = modname + ext - break - if osp.isdir(self.filename): - parent = modules.get(modname) - if parent is None: - item = QTreeWidgetItem(title_item, [module], - QTreeWidgetItem.Type) - item.setIcon(0, get_icon('py.png')) - modules[modname] = item - parent = item - else: - parent = title_item - if len(msg_id) > 1: - text = "[%s] %d : %s" % (msg_id, lineno, message) - else: - text = "%d : %s" % (lineno, message) - msg_item = QTreeWidgetItem(parent, [text], QTreeWidgetItem.Type) - msg_item.setIcon(0, get_icon('arrow.png')) - self.data[id(msg_item)] = (modname, lineno) - - -class PylintWidget(QWidget): - """ - Pylint widget - """ - DATAPATH = get_conf_path('pylint.results') - VERSION = '1.1.0' - - def __init__(self, parent, max_entries=100): - QWidget.__init__(self, parent) - - self.setWindowTitle("Pylint") - - self.output = None - self.error_output = None - - self.max_entries = max_entries - self.rdata = [] - if osp.isfile(self.DATAPATH): - try: - data = pickle.loads(open(self.DATAPATH, 'rb').read()) - if data[0] == self.VERSION: - self.rdata = data[1:] - except (EOFError, ImportError): - pass - - self.filecombo = PythonModulesComboBox(self) - if self.rdata: - self.remove_obsolete_items() - self.filecombo.addItems(self.get_filenames()) - - self.start_button = create_toolbutton(self, icon=get_icon('run.png'), - text=_("Analyze"), - tip=_("Run analysis"), - triggered=self.start, text_beside_icon=True) - self.stop_button = create_toolbutton(self, - icon=get_icon('stop.png'), - text=_("Stop"), - tip=_("Stop current analysis"), - text_beside_icon=True) - self.connect(self.filecombo, SIGNAL('valid(bool)'), - self.start_button.setEnabled) - self.connect(self.filecombo, SIGNAL('valid(bool)'), self.show_data) - - browse_button = create_toolbutton(self, icon=get_icon('fileopen.png'), - tip=_('Select Python file'), - triggered=self.select_file) - - self.ratelabel = QLabel() - self.datelabel = QLabel() - self.log_button = create_toolbutton(self, icon=get_icon('log.png'), - text=_("Output"), - text_beside_icon=True, - tip=_("Complete output"), - triggered=self.show_log) - self.treewidget = ResultsTree(self) - - hlayout1 = QHBoxLayout() - hlayout1.addWidget(self.filecombo) - hlayout1.addWidget(browse_button) - hlayout1.addWidget(self.start_button) - hlayout1.addWidget(self.stop_button) - - hlayout2 = QHBoxLayout() - hlayout2.addWidget(self.ratelabel) - hlayout2.addStretch() - hlayout2.addWidget(self.datelabel) - hlayout2.addStretch() - hlayout2.addWidget(self.log_button) - - layout = QVBoxLayout() - layout.addLayout(hlayout1) - layout.addLayout(hlayout2) - layout.addWidget(self.treewidget) - self.setLayout(layout) - - self.process = None - self.set_running_state(False) - - if PYLINT_PATH is None: - for widget in (self.treewidget, self.filecombo, - self.start_button, self.stop_button): - widget.setDisabled(True) - if os.name == 'nt' \ - and programs.is_module_installed("pylint"): - # Pylint is installed but pylint script is not in PATH - # (AFAIK, could happen only on Windows) - text = _('Pylint script was not found. Please add "%s" to PATH.') - text = to_text_string(text) % osp.join(sys.prefix, "Scripts") - else: - text = _('Please install pylint:') - url = 'http://www.logilab.fr' - text += ' %s' % (url, url) - self.ratelabel.setText(text) - else: - self.show_data() - - def analyze(self, filename): - if PYLINT_PATH is None: - return - filename = to_text_string(filename) # filename is a QString instance - self.kill_if_running() - index, _data = self.get_data(filename) - if index is None: - self.filecombo.addItem(filename) - self.filecombo.setCurrentIndex(self.filecombo.count()-1) - else: - self.filecombo.setCurrentIndex(self.filecombo.findText(filename)) - self.filecombo.selected() - if self.filecombo.is_valid(): - self.start() - - def select_file(self): - self.emit(SIGNAL('redirect_stdio(bool)'), False) - filename, _selfilter = getopenfilename(self, _("Select Python file"), - getcwd(), _("Python files")+" (*.py ; *.pyw)") - self.emit(SIGNAL('redirect_stdio(bool)'), False) - if filename: - self.analyze(filename) - - def remove_obsolete_items(self): - """Removing obsolete items""" - self.rdata = [(filename, data) for filename, data in self.rdata - if is_module_or_package(filename)] - - def get_filenames(self): - return [filename for filename, _data in self.rdata] - - def get_data(self, filename): - filename = osp.abspath(filename) - for index, (fname, data) in enumerate(self.rdata): - if fname == filename: - return index, data - else: - return None, None - - def set_data(self, filename, data): - filename = osp.abspath(filename) - index, _data = self.get_data(filename) - if index is not None: - self.rdata.pop(index) - self.rdata.insert(0, (filename, data)) - self.save() - - def save(self): - while len(self.rdata) > self.max_entries: - self.rdata.pop(-1) - pickle.dump([self.VERSION]+self.rdata, open(self.DATAPATH, 'wb'), 2) - - def show_log(self): - if self.output: - TextEditor(self.output, title=_("Pylint output"), - readonly=True, size=(700, 500)).exec_() - - def start(self): - filename = to_text_string(self.filecombo.currentText()) - - self.process = QProcess(self) - self.process.setProcessChannelMode(QProcess.SeparateChannels) - self.process.setWorkingDirectory(osp.dirname(filename)) - self.connect(self.process, SIGNAL("readyReadStandardOutput()"), - self.read_output) - self.connect(self.process, SIGNAL("readyReadStandardError()"), - lambda: self.read_output(error=True)) - self.connect(self.process, SIGNAL("finished(int,QProcess::ExitStatus)"), - self.finished) - self.connect(self.stop_button, SIGNAL("clicked()"), - self.process.kill) - - self.output = '' - self.error_output = '' - - plver = PYLINT_VER - if plver is not None: - if plver.split('.')[0] == '0': - p_args = ['-i', 'yes'] - else: - # Option '-i' (alias for '--include-ids') was removed in pylint - # 1.0 - p_args = ["--msg-template='{msg_id}:{line:3d},"\ - "{column}: {obj}: {msg}"] - p_args += [osp.basename(filename)] - else: - p_args = [osp.basename(filename)] - self.process.start(PYLINT_PATH, p_args) - - running = self.process.waitForStarted() - self.set_running_state(running) - if not running: - QMessageBox.critical(self, _("Error"), - _("Process failed to start")) - - def set_running_state(self, state=True): - self.start_button.setEnabled(not state) - self.stop_button.setEnabled(state) - - def read_output(self, error=False): - if error: - self.process.setReadChannel(QProcess.StandardError) - else: - self.process.setReadChannel(QProcess.StandardOutput) - qba = QByteArray() - while self.process.bytesAvailable(): - if error: - qba += self.process.readAllStandardError() - else: - qba += self.process.readAllStandardOutput() - text = to_text_string( locale_codec.toUnicode(qba.data()) ) - if error: - self.error_output += text - else: - self.output += text - - def finished(self): - self.set_running_state(False) - if not self.output: - if self.error_output: - QMessageBox.critical(self, _("Error"), self.error_output) - print("pylint error:\n\n" + self.error_output, file=sys.stderr) - return - - # Convention, Refactor, Warning, Error - results = {'C:': [], 'R:': [], 'W:': [], 'E:': []} - txt_module = '************* Module ' - - module = '' # Should not be needed - just in case something goes wrong - for line in self.output.splitlines(): - if line.startswith(txt_module): - # New module - module = line[len(txt_module):] - continue - # Supporting option include-ids: ('R3873:' instead of 'R:') - if not re.match('^[CRWE]+([0-9]{4})?:', line): - continue - i1 = line.find(':') - if i1 == -1: - continue - msg_id = line[:i1] - i2 = line.find(':', i1+1) - if i2 == -1: - continue - line_nb = line[i1+1:i2].strip() - if not line_nb: - continue - line_nb = int(line_nb.split(',')[0]) - message = line[i2+1:] - item = (module, line_nb, message, msg_id) - results[line[0]+':'].append(item) - - # Rate - rate = None - txt_rate = 'Your code has been rated at ' - i_rate = self.output.find(txt_rate) - if i_rate > 0: - i_rate_end = self.output.find('/10', i_rate) - if i_rate_end > 0: - rate = self.output[i_rate+len(txt_rate):i_rate_end] - - # Previous run - previous = '' - if rate is not None: - txt_prun = 'previous run: ' - i_prun = self.output.find(txt_prun, i_rate_end) - if i_prun > 0: - i_prun_end = self.output.find('/10', i_prun) - previous = self.output[i_prun+len(txt_prun):i_prun_end] - - - filename = to_text_string(self.filecombo.currentText()) - self.set_data(filename, (time.localtime(), rate, previous, results)) - self.output = self.error_output + self.output - self.show_data(justanalyzed=True) - - def kill_if_running(self): - if self.process is not None: - if self.process.state() == QProcess.Running: - self.process.kill() - self.process.waitForFinished() - - def show_data(self, justanalyzed=False): - if not justanalyzed: - self.output = None - self.log_button.setEnabled(self.output is not None \ - and len(self.output) > 0) - self.kill_if_running() - filename = to_text_string(self.filecombo.currentText()) - if not filename: - return - - _index, data = self.get_data(filename) - if data is None: - text = _('Source code has not been rated yet.') - self.treewidget.clear_results() - date_text = '' - else: - datetime, rate, previous_rate, results = data - if rate is None: - text = _('Analysis did not succeed ' - '(see output for more details).') - self.treewidget.clear_results() - date_text = '' - else: - text_style = "%s " - rate_style = "%s" - prevrate_style = "%s" - color = "#FF0000" - if float(rate) > 5.: - color = "#22AA22" - elif float(rate) > 3.: - color = "#EE5500" - text = _('Global evaluation:') - text = (text_style % text)+(rate_style % (color, - ('%s/10' % rate))) - if previous_rate: - text_prun = _('previous run:') - text_prun = ' (%s %s/10)' % (text_prun, previous_rate) - text += prevrate_style % text_prun - self.treewidget.set_results(filename, results) - date = to_text_string(time.strftime("%d %b %Y %H:%M", datetime), - encoding='utf8') - date_text = text_style % date - - self.ratelabel.setText(text) - self.datelabel.setText(date_text) - - -def test(): - """Run pylint widget test""" - from spyderlib.utils.qthelpers import qapplication - app = qapplication() - widget = PylintWidget(None) - widget.show() - widget.analyze(__file__) - sys.exit(app.exec_()) - -if __name__ == '__main__': - test() Binärdateien spyder-2.3.8+dfsg1/spyder_profiler/images/profiler.png und spyder-3.0.2+dfsg1/spyder_profiler/images/profiler.png sind verschieden. diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder_profiler/__init__.py spyder-3.0.2+dfsg1/spyder_profiler/__init__.py --- spyder-2.3.8+dfsg1/spyder_profiler/__init__.py 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder_profiler/__init__.py 2016-10-25 02:05:23.000000000 +0200 @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- + +#============================================================================== +# The following statement is required to register this 3rd party plugin: +#============================================================================== +from .profiler import Profiler as PLUGIN_CLASS Binärdateien spyder-2.3.8+dfsg1/spyder_profiler/locale/es/LC_MESSAGES/profiler.mo und spyder-3.0.2+dfsg1/spyder_profiler/locale/es/LC_MESSAGES/profiler.mo sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder_profiler/locale/fr/LC_MESSAGES/profiler.mo und spyder-3.0.2+dfsg1/spyder_profiler/locale/fr/LC_MESSAGES/profiler.mo sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder_profiler/locale/pt_BR/LC_MESSAGES/profiler.mo und spyder-3.0.2+dfsg1/spyder_profiler/locale/pt_BR/LC_MESSAGES/profiler.mo sind verschieden. Binärdateien spyder-2.3.8+dfsg1/spyder_profiler/locale/ru/LC_MESSAGES/profiler.mo und spyder-3.0.2+dfsg1/spyder_profiler/locale/ru/LC_MESSAGES/profiler.mo sind verschieden. diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder_profiler/profiler.py spyder-3.0.2+dfsg1/spyder_profiler/profiler.py --- spyder-2.3.8+dfsg1/spyder_profiler/profiler.py 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder_profiler/profiler.py 2016-11-19 01:31:58.000000000 +0100 @@ -0,0 +1,149 @@ +# -*- coding:utf-8 -*- +# +# Copyright © Spyder Project Contributors +# based on p_pylint.py by Pierre Raybaut +# +# Licensed under the terms of the MIT License +# (see spyder/__init__.py for details) + +"""Profiler Plugin.""" + +# Standard library imports +import os.path as osp + +# Third party imports +from qtpy.QtCore import Qt, Signal +from qtpy.QtWidgets import QGroupBox, QLabel, QVBoxLayout + +# Local imports +from spyder.config.base import get_translation +from spyder.plugins import SpyderPluginMixin +from spyder.plugins.configdialog import PluginConfigPage +from spyder.plugins.runconfig import get_run_configuration +from spyder.utils import icon_manager as ima +from spyder.utils.qthelpers import create_action +from .widgets.profilergui import (ProfilerWidget, is_profiler_installed) + + +_ = get_translation("profiler", "spyder_profiler") + + +class ProfilerConfigPage(PluginConfigPage): + def setup_page(self): + results_group = QGroupBox(_("Results")) + results_label1 = QLabel(_("Profiler plugin results " + "(the output of python's profile/cProfile)\n" + "are stored here:")) + results_label1.setWordWrap(True) + + # Warning: do not try to regroup the following QLabel contents with + # widgets above -- this string was isolated here in a single QLabel + # on purpose: to fix Issue 863 + results_label2 = QLabel(ProfilerWidget.DATAPATH) + + results_label2.setTextInteractionFlags(Qt.TextSelectableByMouse) + results_label2.setWordWrap(True) + + results_layout = QVBoxLayout() + results_layout.addWidget(results_label1) + results_layout.addWidget(results_label2) + results_group.setLayout(results_layout) + + vlayout = QVBoxLayout() + vlayout.addWidget(results_group) + vlayout.addStretch(1) + self.setLayout(vlayout) + + +class Profiler(ProfilerWidget, SpyderPluginMixin): + """Profiler (after python's profile and pstats)""" + CONF_SECTION = 'profiler' + CONFIGWIDGET_CLASS = ProfilerConfigPage + edit_goto = Signal(str, int, str) + + def __init__(self, parent=None): + ProfilerWidget.__init__(self, parent=parent, + max_entries=self.get_option('max_entries', 50)) + SpyderPluginMixin.__init__(self, parent) + + # Initialize plugin + self.initialize_plugin() + + #------ SpyderPluginWidget API --------------------------------------------- + def get_plugin_title(self): + """Return widget title""" + return _("Profiler") + + def get_plugin_icon(self): + """Return widget icon""" + path = osp.join(self.PLUGIN_PATH, self.IMG_PATH) + return ima.icon('profiler', icon_path=path) + + def get_focus_widget(self): + """ + Return the widget to give focus to when + this plugin's dockwidget is raised on top-level + """ + return self.datatree + + def get_plugin_actions(self): + """Return a list of actions related to plugin""" + return [] + + def on_first_registration(self): + """Action to be performed on first plugin registration""" + self.main.tabify_plugins(self.main.help, self) + self.dockwidget.hide() + + def register_plugin(self): + """Register plugin in Spyder's main window""" + self.edit_goto.connect(self.main.editor.load) + self.redirect_stdio.connect(self.main.redirect_internalshell_stdio) + self.main.add_dockwidget(self) + + profiler_act = create_action(self, _("Profile"), + icon=self.get_plugin_icon(), + triggered=self.run_profiler) + profiler_act.setEnabled(is_profiler_installed()) + self.register_shortcut(profiler_act, context="Profiler", + name="Run profiler") + + self.main.run_menu_actions += [profiler_act] + self.main.editor.pythonfile_dependent_actions += [profiler_act] + + def refresh_plugin(self): + """Refresh profiler widget""" + #self.remove_obsolete_items() # FIXME: not implemented yet + + def closing_plugin(self, cancelable=False): + """Perform actions before parent main window is closed""" + return True + + def apply_plugin_settings(self, options): + """Apply configuration file's plugin settings""" + # The history depth option will be applied at + # next Spyder startup, which is soon enough + pass + + #------ Public API --------------------------------------------------------- + def run_profiler(self): + """Run profiler""" + if self.main.editor.save(): + self.analyze(self.main.editor.get_current_filename()) + + def analyze(self, filename): + """Reimplement analyze method""" + if self.dockwidget and not self.ismaximized: + self.dockwidget.setVisible(True) + self.dockwidget.setFocus() + self.dockwidget.raise_() + pythonpath = self.main.get_spyder_pythonpath() + runconf = get_run_configuration(filename) + wdir, args = None, [] + if runconf is not None: + if runconf.wdir_enabled: + wdir = runconf.wdir + if runconf.args_enabled: + args = runconf.args + ProfilerWidget.analyze(self, filename, wdir=wdir, args=args, + pythonpath=pythonpath) diff -Nru --exclude '*.po' --exclude '*.pot' spyder-2.3.8+dfsg1/spyder_profiler/widgets/profilergui.py spyder-3.0.2+dfsg1/spyder_profiler/widgets/profilergui.py --- spyder-2.3.8+dfsg1/spyder_profiler/widgets/profilergui.py 1970-01-01 01:00:00.000000000 +0100 +++ spyder-3.0.2+dfsg1/spyder_profiler/widgets/profilergui.py 2016-11-16 03:30:06.000000000 +0100 @@ -0,0 +1,691 @@ +# -*- coding: utf-8 -*- +# +# Copyright © Spyder Project Contributors +# based on pylintgui.py by Pierre Raybaut +# +# Licensed under the terms of the MIT License +# (see spyder/__init__.py for details) + +""" +Profiler widget + +See the official documentation on python profiling: +http://docs.python.org/library/profile.html +""" + +# Standard library imports +from __future__ import with_statement +import os +import os.path as osp +import sys +import time + +# Third party imports +from qtpy.compat import getopenfilename, getsavefilename +from qtpy.QtCore import (QByteArray, QProcess, QProcessEnvironment, QTextCodec, + Qt, Signal) +from qtpy.QtGui import QColor +from qtpy.QtWidgets import (QApplication, QHBoxLayout, QLabel, QMessageBox, + QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget) + +# Local imports +from spyder.config.base import get_conf_path, get_translation +from spyder.py3compat import getcwd, to_text_string +from spyder.utils import icon_manager as ima +from spyder.utils.qthelpers import (create_toolbutton, get_item_user_text, + set_item_user_text) +from spyder.utils.programs import shell_split +from spyder.widgets.comboboxes import PythonModulesComboBox +from spyder.utils.misc import add_pathlist_to_PYTHONPATH +from spyder.widgets.variableexplorer.texteditor import TextEditor + +# This is needed for testing this module as a stand alone script +try: + _ = get_translation("profiler", "spyder_profiler") +except KeyError as error: + import gettext + _ = gettext.gettext + + +locale_codec = QTextCodec.codecForLocale() + + +def is_profiler_installed(): + from spyder.utils.programs import is_module_installed + return is_module_installed('cProfile') and is_module_installed('pstats') + + +class ProfilerWidget(QWidget): + """ + Profiler widget + """ + DATAPATH = get_conf_path('profiler.results') + VERSION = '0.0.1' + redirect_stdio = Signal(bool) + + def __init__(self, parent, max_entries=100): + QWidget.__init__(self, parent) + + self.setWindowTitle("Profiler") + + self.output = None + self.error_output = None + + self._last_wdir = None + self._last_args = None + self._last_pythonpath = None + + self.filecombo = PythonModulesComboBox(self) + + self.start_button = create_toolbutton(self, icon=ima.icon('run'), + text=_("Profile"), + tip=_("Run profiler"), + triggered=lambda : self.start(), + text_beside_icon=True) + self.stop_button = create_toolbutton(self, + icon=ima.icon('stop'), + text=_("Stop"), + tip=_("Stop current profiling"), + text_beside_icon=True) + self.filecombo.valid.connect(self.start_button.setEnabled) + #self.connect(self.filecombo, SIGNAL('valid(bool)'), self.show_data) + # FIXME: The combobox emits this signal on almost any event + # triggering show_data() too early, too often. + + browse_button = create_toolbutton(self, icon=ima.icon('fileopen'), + tip=_('Select Python script'), + triggered=self.select_file) + + self.datelabel = QLabel() + + self.log_button = create_toolbutton(self, icon=ima.icon('log'), + text=_("Output"), + text_beside_icon=True, + tip=_("Show program's output"), + triggered=self.show_log) + + self.datatree = ProfilerDataTree(self) + + self.collapse_button = create_toolbutton(self, + icon=ima.icon('collapse'), + triggered=lambda dD: + self.datatree.change_view(-1), + tip=_('Collapse one level up')) + self.expand_button = create_toolbutton(self, + icon=ima.icon('expand'), + triggered=lambda dD: + self.datatree.change_view(1), + tip=_('Expand one level down')) + + self.save_button = create_toolbutton(self, text_beside_icon=True, + text=_("Save data"), + icon=ima.icon('filesave'), + triggered=self.save_data, + tip=_('Save profiling data')) + self.load_button = create_toolbutton(self, text_beside_icon=True, + text=_("Load data"), + icon=ima.icon('fileimport'), + triggered=self.compare, + tip=_('Load profiling data for comparison')) + self.clear_button = create_toolbutton(self, text_beside_icon=True, + text=_("Clear comparison"), + icon=ima.icon('editdelete'), + triggered=self.clear) + + hlayout1 = QHBoxLayout() + hlayout1.addWidget(self.filecombo) + hlayout1.addWidget(browse_button) + hlayout1.addWidget(self.start_button) + hlayout1.addWidget(self.stop_button) + + hlayout2 = QHBoxLayout() + hlayout2.addWidget(self.collapse_button) + hlayout2.addWidget(self.expand_button) + hlayout2.addStretch() + hlayout2.addWidget(self.datelabel) + hlayout2.addStretch() + hlayout2.addWidget(self.log_button) + hlayout2.addWidget(self.save_button) + hlayout2.addWidget(self.load_button) + hlayout2.addWidget(self.clear_button) + + layout = QVBoxLayout() + layout.addLayout(hlayout1) + layout.addLayout(hlayout2) + layout.addWidget(self.datatree) + self.setLayout(layout) + + self.process = None + self.set_running_state(False) + self.start_button.setEnabled(False) + self.clear_button.setEnabled(False) + + if not is_profiler_installed(): + # This should happen only on certain GNU/Linux distributions + # or when this a home-made Python build because the Python + # profilers are included in the Python standard library + for widget in (self.datatree, self.filecombo, + self.start_button, self.stop_button): + widget.setDisabled(True) + url = 'http://docs.python.org/library/profile.html' + text = '%s %s' % (_('Please install'), url, + _("the Python profiler modules")) + self.datelabel.setText(text) + else: + pass # self.show_data() + + def save_data(self): + """Save data""" + title = _( "Save profiler result") + filename, _selfilter = getsavefilename(self, title, + getcwd(), + _("Profiler result")+" (*.Result)") + if filename: + self.datatree.save_data(filename) + + def compare(self): + filename, _selfilter = getopenfilename(self, _("Select script to compare"), + getcwd(), _("Profiler result")+" (*.Result)") + if filename: + self.datatree.compare(filename) + self.show_data() + self.clear_button.setEnabled(True) + + def clear(self): + self.datatree.compare(None) + self.datatree.hide_diff_cols(True) + self.show_data() + self.clear_button.setEnabled(False) + + def analyze(self, filename, wdir=None, args=None, pythonpath=None): + if not is_profiler_installed(): + return + self.kill_if_running() + #index, _data = self.get_data(filename) + index = None # FIXME: storing data is not implemented yet + if index is None: + self.filecombo.addItem(filename) + self.filecombo.setCurrentIndex(self.filecombo.count()-1) + else: + self.filecombo.setCurrentIndex(self.filecombo.findText(filename)) + self.filecombo.selected() + if self.filecombo.is_valid(): + if wdir is None: + wdir = osp.dirname(filename) + self.start(wdir, args, pythonpath) + + def select_file(self): + self.redirect_stdio.emit(False) + filename, _selfilter = getopenfilename(self, _("Select Python script"), + getcwd(), _("Python scripts")+" (*.py ; *.pyw)") + self.redirect_stdio.emit(True) + if filename: + self.analyze(filename) + + def show_log(self): + if self.output: + TextEditor(self.output, title=_("Profiler output"), + readonly=True, size=(700, 500)).exec_() + + def show_errorlog(self): + if self.error_output: + TextEditor(self.error_output, title=_("Profiler output"), + readonly=True, size=(700, 500)).exec_() + + def start(self, wdir=None, args=None, pythonpath=None): + filename = to_text_string(self.filecombo.currentText()) + if wdir is None: + wdir = self._last_wdir + if wdir is None: + wdir = osp.basename(filename) + if args is None: + args = self._last_args + if args is None: + args = [] + if pythonpath is None: + pythonpath = self._last_pythonpath + self._last_wdir = wdir + self._last_args = args + self._last_pythonpath = pythonpath + + self.datelabel.setText(_('Profiling, please wait...')) + + self.process = QProcess(self) + self.process.setProcessChannelMode(QProcess.SeparateChannels) + self.process.setWorkingDirectory(wdir) + self.process.readyReadStandardOutput.connect(self.read_output) + self.process.readyReadStandardError.connect( + lambda: self.read_output(error=True)) + self.process.finished.connect(lambda ec, es=QProcess.ExitStatus: + self.finished(ec, es)) + self.stop_button.clicked.connect(self.process.kill) + + if pythonpath is not None: + env = [to_text_string(_pth) + for _pth in self.process.systemEnvironment()] + add_pathlist_to_PYTHONPATH(env, pythonpath) + processEnvironment = QProcessEnvironment() + for envItem in env: + envName, separator, envValue = envItem.partition('=') + processEnvironment.insert(envName, envValue) + self.process.setProcessEnvironment(processEnvironment) + + self.output = '' + self.error_output = '' + + p_args = ['-m', 'cProfile', '-o', self.DATAPATH] + if os.name == 'nt': + # On Windows, one has to replace backslashes by slashes to avoid + # confusion with escape characters (otherwise, for example, '\t' + # will be interpreted as a tabulation): + p_args.append(osp.normpath(filename).replace(os.sep, '/')) + else: + p_args.append(filename) + if args: + p_args.extend(shell_split(args)) + executable = sys.executable + if executable.endswith("spyder.exe"): + # py2exe distribution + executable = "python.exe" + self.process.start(executable, p_args) + + running = self.process.waitForStarted() + self.set_running_state(running) + if not running: + QMessageBox.critical(self, _("Error"), + _("Process failed to start")) + + def set_running_state(self, state=True): + self.start_button.setEnabled(not state) + self.stop_button.setEnabled(state) + + def read_output(self, error=False): + if error: + self.process.setReadChannel(QProcess.StandardError) + else: + self.process.setReadChannel(QProcess.StandardOutput) + qba = QByteArray() + while self.process.bytesAvailable(): + if error: + qba += self.process.readAllStandardError() + else: + qba += self.process.readAllStandardOutput() + text = to_text_string( locale_codec.toUnicode(qba.data()) ) + if error: + self.error_output += text + else: + self.output += text + + def finished(self, exit_code, exit_status): + self.set_running_state(False) + self.show_errorlog() # If errors occurred, show them. + self.output = self.error_output + self.output + # FIXME: figure out if show_data should be called here or + # as a signal from the combobox + self.show_data(justanalyzed=True) + + def kill_if_running(self): + if self.process is not None: + if self.process.state() == QProcess.Running: + self.process.kill() + self.process.waitForFinished() + + def show_data(self, justanalyzed=False): + if not justanalyzed: + self.output = None + self.log_button.setEnabled(self.output is not None \ + and len(self.output) > 0) + self.kill_if_running() + filename = to_text_string(self.filecombo.currentText()) + if not filename: + return + + self.datelabel.setText(_('Sorting data, please wait...')) + QApplication.processEvents() + + self.datatree.load_data(self.DATAPATH) + self.datatree.show_tree() + + text_style = "%s " + date_text = text_style % time.strftime("%d %b %Y %H:%M", + time.localtime()) + self.datelabel.setText(date_text) + + +class TreeWidgetItem( QTreeWidgetItem ): + def __init__(self, parent=None): + QTreeWidgetItem.__init__(self, parent) + + def __lt__(self, otherItem): + column = self.treeWidget().sortColumn() + try: + return float( self.text(column) ) > float( otherItem.text(column) ) + except ValueError: + return self.text(column) > otherItem.text(column) + + +class ProfilerDataTree(QTreeWidget): + """ + Convenience tree widget (with built-in model) + to store and view profiler data. + + The quantities calculated by the profiler are as follows + (from profile.Profile): + [0] = The number of times this function was called, not counting direct + or indirect recursion, + [1] = Number of times this function appears on the stack, minus one + [2] = Total time spent internal to this function + [3] = Cumulative time that this function was present on the stack. In + non-recursive functions, this is the total execution time from start + to finish of each invocation of a function, including time spent in + all subfunctions. + [4] = A dictionary indicating for each function name, the number of times + it was called by us. + """ + SEP = r"<[=]>" # separator between filename and linenumber + # (must be improbable as a filename to avoid splitting the filename itself) + def __init__(self, parent=None): + QTreeWidget.__init__(self, parent) + self.header_list = [_('Function/Module'), _('Total Time'), _('Diff'), + _('Local Time'), _('Diff'), _('Calls'), _('Diff'), + _('File:line')] + self.icon_list = {'module': ima.icon('python'), + 'function': ima.icon('function'), + 'builtin': ima.icon('python_t'), + 'constructor': ima.icon('class')} + self.profdata = None # To be filled by self.load_data() + self.stats = None # To be filled by self.load_data() + self.item_depth = None + self.item_list = None + self.items_to_be_shown = None + self.current_view_depth = None + self.compare_file = None + self.setColumnCount(len(self.header_list)) + self.setHeaderLabels(self.header_list) + self.initialize_view() + self.itemActivated.connect(self.item_activated) + self.itemExpanded.connect(self.item_expanded) + + def set_item_data(self, item, filename, line_number): + """Set tree item user data: filename (string) and line_number (int)""" + set_item_user_text(item, '%s%s%d' % (filename, self.SEP, line_number)) + + def get_item_data(self, item): + """Get tree item user data: (filename, line_number)""" + filename, line_number_str = get_item_user_text(item).split(self.SEP) + return filename, int(line_number_str) + + def initialize_view(self): + """Clean the tree and view parameters""" + self.clear() + self.item_depth = 0 # To be use for collapsing/expanding one level + self.item_list = [] # To be use for collapsing/expanding one level + self.items_to_be_shown = {} + self.current_view_depth = 0 + + def load_data(self, profdatafile): + """Load profiler data saved by profile/cProfile module""" + import pstats + stats_indi = [pstats.Stats(profdatafile),] + self.profdata = stats_indi[0] + + if self.compare_file is not None: + stats_indi.append(pstats.Stats(self.compare_file)) + map(lambda x: x.calc_callees(), stats_indi) + self.profdata.calc_callees() + self.stats1 = stats_indi + self.stats = stats_indi[0].stats + + def compare(self,filename): + self.hide_diff_cols(False) + self.compare_file = filename + + def hide_diff_cols(self, hide): + for i in (2,4,6): + self.setColumnHidden(i, hide) + + def save_data(self, filename): + """""" + self.stats1[0].dump_stats(filename) + + def find_root(self): + """Find a function without a caller""" + self.profdata.sort_stats("cumulative") + for func in self.profdata.fcn_list: + if ('~', 0) != func[0:2] and not func[2].startswith( + ''): + # This skips the profiler function at the top of the list + # it does only occur in Python 3 + return func + + def find_callees(self, parent): + """Find all functions called by (parent) function.""" + # FIXME: This implementation is very inneficient, because it + # traverses all the data to find children nodes (callees) + return self.profdata.all_callees[parent] + + def show_tree(self): + """Populate the tree with profiler data and display it.""" + self.initialize_view() # Clear before re-populating + self.setItemsExpandable(True) + self.setSortingEnabled(False) + rootkey = self.find_root() # This root contains profiler overhead + if rootkey: + self.populate_tree(self, self.find_callees(rootkey)) + self.resizeColumnToContents(0) + self.setSortingEnabled(True) + self.sortItems(1, Qt.AscendingOrder) # FIXME: hardcoded index + self.change_view(1) + + def function_info(self, functionKey): + """Returns processed information about the function's name and file.""" + node_type = 'function' + filename, line_number, function_name = functionKey + if function_name == '': + modulePath, moduleName = osp.split(filename) + node_type = 'module' + if moduleName == '__init__.py': + modulePath, moduleName = osp.split(modulePath) + function_name = '<' + moduleName + '>' + if not filename or filename == '~': + file_and_line = '(built-in)' + node_type = 'builtin' + else: + if function_name == '__init__': + node_type = 'constructor' + file_and_line = '%s : %d' % (filename, line_number) + return filename, line_number, function_name, file_and_line, node_type + + def color_string(self, args): + x, format = args + diff_str = "" + color = "black" + if len(x) == 2 and self.compare_file is not None: + difference = x[0] - x[1] + if difference < 0: + diff_str = "".join(['',format[1] % difference]) + color = "green" + elif difference > 0: + diff_str = "".join(['+',format[1] % difference]) + color = "red" + return [format[0] % x[0], [diff_str, color]] + + + def format_output(self,child_key): + """ Formats the data""" + if True: + data = [x.stats.get(child_key,[0,0,0,0,0]) for x in self.stats1] + return map(self.color_string,zip(list(zip(*data))[1:4], [["%i"]*2, ["%.3f","%.3f"], ["%.3f","%.3f"]])) + + def populate_tree(self, parentItem, children_list): + """Recursive method to create each item (and associated data) in the tree.""" + for child_key in children_list: + self.item_depth += 1 + (filename, line_number, function_name, file_and_line, node_type + ) = self.function_info(child_key) + + ((total_calls, total_calls_dif), (loc_time, loc_time_dif), (cum_time, + cum_time_dif)) = self.format_output(child_key) + + (primcalls, total_calls, loc_time, cum_time, callers + ) = self.stats[child_key] + child_item = TreeWidgetItem(parentItem) + self.item_list.append(child_item) + self.set_item_data(child_item, filename, line_number) + + # FIXME: indexes to data should be defined by a dictionary on init + child_item.setToolTip(0, _('Function or module name')) + child_item.setData(0, Qt.DisplayRole, function_name) + child_item.setIcon(0, self.icon_list[node_type]) + + child_item.setToolTip(1, _('Time in function '\ + '(including sub-functions)')) + child_item.setData(1, Qt.DisplayRole, cum_time) + child_item.setTextAlignment(1, Qt.AlignRight) + + child_item.setData(2, Qt.DisplayRole, cum_time_dif[0]) + child_item.setForeground(2, QColor(cum_time_dif[1])) + child_item.setTextAlignment(2, Qt.AlignLeft) + + child_item.setToolTip(3, _('Local time in function '\ + '(not in sub-functions)')) + + child_item.setData(3, Qt.DisplayRole, loc_time) + child_item.setTextAlignment(3, Qt.AlignRight) + + child_item.setData(4, Qt.DisplayRole, loc_time_dif[0]) + child_item.setForeground(4, QColor(loc_time_dif[1])) + child_item.setTextAlignment(4, Qt.AlignLeft) + + child_item.setToolTip(5, _('Total number of calls '\ + '(including recursion)')) + + child_item.setData(5, Qt.DisplayRole, total_calls) + child_item.setTextAlignment(5, Qt.AlignRight) + + child_item.setData(6, Qt.DisplayRole, total_calls_dif[0]) + child_item.setForeground(6, QColor(total_calls_dif[1])) + child_item.setTextAlignment(6, Qt.AlignLeft) + + child_item.setToolTip(7, _('File:line '\ + 'where function is defined')) + child_item.setData(7, Qt.DisplayRole, file_and_line) + #child_item.setExpanded(True) + if self.is_recursive(child_item): + child_item.setData(7, Qt.DisplayRole, '(%s)' % _('recursion')) + child_item.setDisabled(True) + else: + callees = self.find_callees(child_key) + if self.item_depth < 3: + self.populate_tree(child_item, callees) + elif callees: + child_item.setChildIndicatorPolicy(child_item.ShowIndicator) + self.items_to_be_shown[id(child_item)] = callees + self.item_depth -= 1 + + def item_activated(self, item): + filename, line_number = self.get_item_data(item) + self.parent().edit_goto.emit(filename, line_number, '') + + def item_expanded(self, item): + if item.childCount() == 0 and id(item) in self.items_to_be_shown: + callees = self.items_to_be_shown[id(item)] + self.populate_tree(item, callees) + + def is_recursive(self, child_item): + """Returns True is a function is a descendant of itself.""" + ancestor = child_item.parent() + # FIXME: indexes to data should be defined by a dictionary on init + while ancestor: + if (child_item.data(0, Qt.DisplayRole + ) == ancestor.data(0, Qt.DisplayRole) and + child_item.data(4, Qt.DisplayRole + ) == ancestor.data(4, Qt.DisplayRole)): + return True + else: + ancestor = ancestor.parent() + return False + + def get_top_level_items(self): + """Iterate over top level items""" + return [self.topLevelItem(_i) for _i in range(self.topLevelItemCount())] + + def get_items(self, maxlevel): + """Return all items with a level <= `maxlevel`""" + itemlist = [] + def add_to_itemlist(item, maxlevel, level=1): + level += 1 + for index in range(item.childCount()): + citem = item.child(index) + itemlist.append(citem) + if level <= maxlevel: + add_to_itemlist(citem, maxlevel, level) + for tlitem in self.get_top_level_items(): + itemlist.append(tlitem) + if maxlevel > 0: + add_to_itemlist(tlitem, maxlevel=maxlevel) + return itemlist + + def change_view(self, change_in_depth): + """Change the view depth by expand or collapsing all same-level nodes""" + self.current_view_depth += change_in_depth + if self.current_view_depth < 0: + self.current_view_depth = 0 + self.collapseAll() + if self.current_view_depth > 0: + for item in self.get_items(maxlevel=self.current_view_depth-1): + item.setExpanded(True) + + +#============================================================================== +# Tests +#============================================================================== +def primes(n): + """ + Simple test function + Taken from http://www.huyng.com/posts/python-performance-analysis/ + """ + if n==2: + return [2] + elif n<2: + return [] + s=list(range(3,n+1,2)) + mroot = n ** 0.5 + half=(n+1)//2-1 + i=0 + m=3 + while m <= mroot: + if s[i]: + j=(m*m-3)//2 + s[j]=0 + while j1 else '') + title_item = QTreeWidgetItem(self, [title], QTreeWidgetItem.Type) + title_item.setIcon(0, icon) + if not messages: + title_item.setDisabled(True) + modules = {} + for module, lineno, message, msg_id in messages: + basename = osp.splitext(osp.basename(self.filename))[0] + if not module.startswith(basename): + # Pylint bug + i_base = module.find(basename) + module = module[i_base:] + dirname = osp.dirname(self.filename) + if module.startswith('.') or module == basename: + modname = osp.join(dirname, module) + else: + modname = osp.join(dirname, *module.split('.')) + if osp.isdir(modname): + modname = osp.join(modname, '__init__') + for ext in ('.py', '.pyw'): + if osp.isfile(modname+ext): + modname = modname + ext + break + if osp.isdir(self.filename): + parent = modules.get(modname) + if parent is None: + item = QTreeWidgetItem(title_item, [module], + QTreeWidgetItem.Type) + item.setIcon(0, ima.icon('python')) + modules[modname] = item + parent = item + else: + parent = title_item + if len(msg_id) > 1: + text = "[%s] %d : %s" % (msg_id, lineno, message) + else: + text = "%d : %s" % (lineno, message) + msg_item = QTreeWidgetItem(parent, [text], QTreeWidgetItem.Type) + msg_item.setIcon(0, ima.icon('arrow')) + self.data[id(msg_item)] = (modname, lineno) + + +class PylintWidget(QWidget): + """ + Pylint widget + """ + DATAPATH = get_conf_path('pylint.results') + VERSION = '1.1.0' + redirect_stdio = Signal(bool) + + def __init__(self, parent, max_entries=100): + QWidget.__init__(self, parent) + + self.setWindowTitle("Pylint") + + self.output = None + self.error_output = None + + self.max_entries = max_entries + self.rdata = [] + if osp.isfile(self.DATAPATH): + try: + data = pickle.loads(open(self.DATAPATH, 'rb').read()) + if data[0] == self.VERSION: + self.rdata = data[1:] + except (EOFError, ImportError): + pass + + self.filecombo = PythonModulesComboBox(self) + if self.rdata: + self.remove_obsolete_items() + self.filecombo.addItems(self.get_filenames()) + + self.start_button = create_toolbutton(self, icon=ima.icon('run'), + text=_("Analyze"), + tip=_("Run analysis"), + triggered=self.start, text_beside_icon=True) + self.stop_button = create_toolbutton(self, + icon=ima.icon('stop'), + text=_("Stop"), + tip=_("Stop current analysis"), + text_beside_icon=True) + self.filecombo.valid.connect(self.start_button.setEnabled) + self.filecombo.valid.connect(self.show_data) + + browse_button = create_toolbutton(self, icon=ima.icon('fileopen'), + tip=_('Select Python file'), + triggered=self.select_file) + + self.ratelabel = QLabel() + self.datelabel = QLabel() + self.log_button = create_toolbutton(self, icon=ima.icon('log'), + text=_("Output"), + text_beside_icon=True, + tip=_("Complete output"), + triggered=self.show_log) + self.treewidget = ResultsTree(self) + + hlayout1 = QHBoxLayout() + hlayout1.addWidget(self.filecombo) + hlayout1.addWidget(browse_button) + hlayout1.addWidget(self.start_button) + hlayout1.addWidget(self.stop_button) + + hlayout2 = QHBoxLayout() + hlayout2.addWidget(self.ratelabel) + hlayout2.addStretch() + hlayout2.addWidget(self.datelabel) + hlayout2.addStretch() + hlayout2.addWidget(self.log_button) + + layout = QVBoxLayout() + layout.addLayout(hlayout1) + layout.addLayout(hlayout2) + layout.addWidget(self.treewidget) + self.setLayout(layout) + + self.process = None + self.set_running_state(False) + + if PYLINT_PATH is None: + for widget in (self.treewidget, self.filecombo, + self.start_button, self.stop_button): + widget.setDisabled(True) + if os.name == 'nt' \ + and programs.is_module_installed("pylint"): + # Pylint is installed but pylint script is not in PATH + # (AFAIK, could happen only on Windows) + text = _('Pylint script was not found. Please add "%s" to PATH.') + text = to_text_string(text) % osp.join(sys.prefix, "Scripts") + else: + text = _('Please install pylint:') + url = 'http://www.logilab.fr' + text += ' %s' % (url, url) + self.ratelabel.setText(text) + else: + self.show_data() + + def analyze(self, filename): + if PYLINT_PATH is None: + return + filename = to_text_string(filename) # filename is a QString instance + self.kill_if_running() + index, _data = self.get_data(filename) + if index is None: + self.filecombo.addItem(filename) + self.filecombo.setCurrentIndex(self.filecombo.count()-1) + else: + self.filecombo.setCurrentIndex(self.filecombo.findText(filename)) + self.filecombo.selected() + if self.filecombo.is_valid(): + self.start() + + @Slot() + def select_file(self): + self.redirect_stdio.emit(False) + filename, _selfilter = getopenfilename(self, _("Select Python file"), + getcwd(), _("Python files")+" (*.py ; *.pyw)") + self.redirect_stdio.emit(True) + if filename: + self.analyze(filename) + + def remove_obsolete_items(self): + """Removing obsolete items""" + self.rdata = [(filename, data) for filename, data in self.rdata + if is_module_or_package(filename)] + + def get_filenames(self): + return [filename for filename, _data in self.rdata] + + def get_data(self, filename): + filename = osp.abspath(filename) + for index, (fname, data) in enumerate(self.rdata): + if fname == filename: + return index, data + else: + return None, None + + def set_data(self, filename, data): + filename = osp.abspath(filename) + index, _data = self.get_data(filename) + if index is not None: + self.rdata.pop(index) + self.rdata.insert(0, (filename, data)) + self.save() + + def save(self): + while len(self.rdata) > self.max_entries: + self.rdata.pop(-1) + pickle.dump([self.VERSION]+self.rdata, open(self.DATAPATH, 'wb'), 2) + + @Slot() + def show_log(self): + if self.output: + TextEditor(self.output, title=_("Pylint output"), + readonly=True, size=(700, 500)).exec_() + + @Slot() + def start(self): + filename = to_text_string(self.filecombo.currentText()) + + self.process = QProcess(self) + self.process.setProcessChannelMode(QProcess.SeparateChannels) + self.process.setWorkingDirectory(osp.dirname(filename)) + self.process.readyReadStandardOutput.connect(self.read_output) + self.process.readyReadStandardError.connect( + lambda: self.read_output(error=True)) + self.process.finished.connect(lambda ec, es=QProcess.ExitStatus: + self.finished(ec, es)) + self.stop_button.clicked.connect(self.process.kill) + + self.output = '' + self.error_output = '' + + plver = PYLINT_VER + if plver is not None: + if plver.split('.')[0] == '0': + p_args = ['-i', 'yes'] + else: + # Option '-i' (alias for '--include-ids') was removed in pylint + # 1.0 + p_args = ["--msg-template='{msg_id}:{line:3d},"\ + "{column}: {obj}: {msg}"] + p_args += [osp.basename(filename)] + else: + p_args = [osp.basename(filename)] + self.process.start(PYLINT_PATH, p_args) + + running = self.process.waitForStarted() + self.set_running_state(running) + if not running: + QMessageBox.critical(self, _("Error"), + _("Process failed to start")) + + def set_running_state(self, state=True): + self.start_button.setEnabled(not state) + self.stop_button.setEnabled(state) + + def read_output(self, error=False): + if error: + self.process.setReadChannel(QProcess.StandardError) + else: + self.process.setReadChannel(QProcess.StandardOutput) + qba = QByteArray() + while self.process.bytesAvailable(): + if error: + qba += self.process.readAllStandardError() + else: + qba += self.process.readAllStandardOutput() + text = to_text_string( locale_codec.toUnicode(qba.data()) ) + if error: + self.error_output += text + else: + self.output += text + + def finished(self, exit_code, exit_status): + self.set_running_state(False) + if not self.output: + if self.error_output: + QMessageBox.critical(self, _("Error"), self.error_output) + print("pylint error:\n\n" + self.error_output, file=sys.stderr) + return + + # Convention, Refactor, Warning, Error + results = {'C:': [], 'R:': [], 'W:': [], 'E:': []} + txt_module = '************* Module ' + + module = '' # Should not be needed - just in case something goes wrong + for line in self.output.splitlines(): + if line.startswith(txt_module): + # New module + module = line[len(txt_module):] + continue + # Supporting option include-ids: ('R3873:' instead of 'R:') + if not re.match('^[CRWE]+([0-9]{4})?:', line): + continue + i1 = line.find(':') + if i1 == -1: + continue + msg_id = line[:i1] + i2 = line.find(':', i1+1) + if i2 == -1: + continue + line_nb = line[i1+1:i2].strip() + if not line_nb: + continue + line_nb = int(line_nb.split(',')[0]) + message = line[i2+1:] + item = (module, line_nb, message, msg_id) + results[line[0]+':'].append(item) + + # Rate + rate = None + txt_rate = 'Your code has been rated at ' + i_rate = self.output.find(txt_rate) + if i_rate > 0: + i_rate_end = self.output.find('/10', i_rate) + if i_rate_end > 0: + rate = self.output[i_rate+len(txt_rate):i_rate_end] + + # Previous run + previous = '' + if rate is not None: + txt_prun = 'previous run: ' + i_prun = self.output.find(txt_prun, i_rate_end) + if i_prun > 0: + i_prun_end = self.output.find('/10', i_prun) + previous = self.output[i_prun+len(txt_prun):i_prun_end] + + + filename = to_text_string(self.filecombo.currentText()) + self.set_data(filename, (time.localtime(), rate, previous, results)) + self.output = self.error_output + self.output + self.show_data(justanalyzed=True) + + def kill_if_running(self): + if self.process is not None: + if self.process.state() == QProcess.Running: + self.process.kill() + self.process.waitForFinished() + + def show_data(self, justanalyzed=False): + if not justanalyzed: + self.output = None + self.log_button.setEnabled(self.output is not None \ + and len(self.output) > 0) + self.kill_if_running() + filename = to_text_string(self.filecombo.currentText()) + if not filename: + return + + _index, data = self.get_data(filename) + if data is None: + text = _('Source code has not been rated yet.') + self.treewidget.clear_results() + date_text = '' + else: + datetime, rate, previous_rate, results = data + if rate is None: + text = _('Analysis did not succeed ' + '(see output for more details).') + self.treewidget.clear_results() + date_text = '' + else: + text_style = "%s " + rate_style = "%s" + prevrate_style = "%s" + color = "#FF0000" + if float(rate) > 5.: + color = "#22AA22" + elif float(rate) > 3.: + color = "#EE5500" + text = _('Global evaluation:') + text = (text_style % text)+(rate_style % (color, + ('%s/10' % rate))) + if previous_rate: + text_prun = _('previous run:') + text_prun = ' (%s %s/10)' % (text_prun, previous_rate) + text += prevrate_style % text_prun + self.treewidget.set_results(filename, results) + date = to_text_string(time.strftime("%d %b %Y %H:%M", datetime), + encoding='utf8') + date_text = text_style % date + + self.ratelabel.setText(text) + self.datelabel.setText(date_text) + + +#============================================================================== +# Tests +#============================================================================== +def test(): + """Run pylint widget test""" + from spyder.utils.qthelpers import qapplication + app = qapplication(test_time=20) + widget = PylintWidget(None) + widget.resize(640, 480) + widget.show() + widget.analyze(__file__) + sys.exit(app.exec_()) + + +if __name__ == '__main__': + test()