diff --git a/external-deps/spyder-kernels/.gitrepo b/external-deps/spyder-kernels/.gitrepo index 8289cc5780e..6211a979227 100644 --- a/external-deps/spyder-kernels/.gitrepo +++ b/external-deps/spyder-kernels/.gitrepo @@ -4,9 +4,9 @@ ; git-subrepo command. See https://github.com/git-commands/git-subrepo#readme ; [subrepo] - remote = https://github.com/spyder-ide/spyder-kernels.git - branch = master - commit = 34a6d634383f27c61cd92172be94c1b344bc5cf1 - parent = f80c897ac6aafb65c421da50a63e2092f32ff7e2 + remote = https://github.com/PierreRaybaut/spyder-kernels.git + branch = improving_guiqwt_integration_kernel + commit = 6c5418b2866bcd558bd9c4227ff7584b601bed28 + parent = 2b75726b538443ca0140cfa6ed02ab75c5bb8318 method = merge cmdver = 0.4.5 diff --git a/external-deps/spyder-kernels/spyder_kernels/console/start.py b/external-deps/spyder-kernels/spyder_kernels/console/start.py index f5e4b122b0c..0dae58077ff 100644 --- a/external-deps/spyder-kernels/spyder_kernels/console/start.py +++ b/external-deps/spyder-kernels/spyder_kernels/console/start.py @@ -255,12 +255,20 @@ def varexp(line): ip = get_ipython() #analysis:ignore funcname, name = line.split() try: - import guiqwt.pyplot as pyplot - except: - import matplotlib.pyplot as pyplot - pyplot.figure(); - getattr(pyplot, funcname[2:])(ip.kernel._get_current_namespace()[name]) - pyplot.show() + data = ip.kernel._get_current_namespace()[name] + except KeyError: + print("\033[41mVariable %s does not exist in current namespace.\033[0m" % name) + plotlib = os.environ.get('SPY_VAREXP_PLOTLIB') + try: + pyplot = __import__(plotlib + '.pyplot', globals(), locals(), [], 0).pyplot + pyplot.figure() + getattr(pyplot, funcname[2:])(data) + pyplot.show() + except (ImportError, ModuleNotFoundError): + print("\033[41mPlease install %s or select an available plotting library " + "in Variable Explorer preferences.\033[0m" % plotlib) + except Exception: + ip.showtraceback(sys.exc_info()) def main(): diff --git a/spyder/config/main.py b/spyder/config/main.py index cd06e14ae92..1b20e2e0661 100644 --- a/spyder/config/main.py +++ b/spyder/config/main.py @@ -21,6 +21,7 @@ from spyder.config.appearance import APPEARANCE from spyder.plugins.editor.utils.findtasks import TASKS_PATTERN from spyder.utils.introspection.module_completion import PREFERRED_MODULES +from spyder.plugins.variableexplorer.plotlib import DEFAULT_PLOTLIB # ============================================================================= @@ -176,7 +177,8 @@ 'truncate': True, 'minmax': False, 'show_callable_attributes': True, - 'show_special_attributes': False + 'show_special_attributes': False, + 'plotlib': DEFAULT_PLOTLIB, }), ('debugger', { diff --git a/spyder/plugins/ipythonconsole/utils/kernelspec.py b/spyder/plugins/ipythonconsole/utils/kernelspec.py index ae6ced09d71..af290dd25a8 100644 --- a/spyder/plugins/ipythonconsole/utils/kernelspec.py +++ b/spyder/plugins/ipythonconsole/utils/kernelspec.py @@ -208,6 +208,7 @@ def env(self): 'SPY_GREEDY_O': self.get_conf('greedy_completer'), 'SPY_JEDI_O': self.get_conf('jedi_completer'), 'SPY_SYMPY_O': self.get_conf('symbolic_math'), + 'SPY_VAREXP_PLOTLIB': self.get_conf('plotlib', section='variable_explorer'), 'SPY_TESTING': running_under_pytest() or get_safe_mode(), 'SPY_HIDE_CMD': self.get_conf('hide_cmd_windows'), 'SPY_PYTHONPATH': pypath diff --git a/spyder/plugins/plots/widgets/figurebrowser.py b/spyder/plugins/plots/widgets/figurebrowser.py index c619feb193e..74bda0626e2 100644 --- a/spyder/plugins/plots/widgets/figurebrowser.py +++ b/spyder/plugins/plots/widgets/figurebrowser.py @@ -772,8 +772,6 @@ def add_thumbnail(self, fig, fmt): parent=self, background_color=self.background_color) thumbnail.canvas.load_figure(fig, fmt) thumbnail.sig_canvas_clicked.connect(self.set_current_thumbnail) - thumbnail.sig_remove_figure_requested.connect(self.remove_thumbnail) - thumbnail.sig_save_figure_requested.connect(self.save_figure_as) thumbnail.sig_context_menu_requested.connect( lambda point: self.show_context_menu(point, thumbnail)) self._thumbnails.append(thumbnail) @@ -796,8 +794,6 @@ def remove_all_thumbnails(self): """Remove all thumbnails.""" for thumbnail in self._thumbnails: thumbnail.sig_canvas_clicked.disconnect() - thumbnail.sig_remove_figure_requested.disconnect() - thumbnail.sig_save_figure_requested.disconnect() self.layout().removeWidget(thumbnail) thumbnail.setParent(None) thumbnail.hide() @@ -815,8 +811,6 @@ def remove_thumbnail(self, thumbnail): # Disconnect signals try: thumbnail.sig_canvas_clicked.disconnect() - thumbnail.sig_remove_figure_requested.disconnect() - thumbnail.sig_save_figure_requested.disconnect() except TypeError: pass @@ -945,29 +939,6 @@ class FigureThumbnail(QWidget): The clicked figure thumbnail. """ - sig_remove_figure_requested = Signal(object) - """ - This signal is emitted to request the removal of a figure thumbnail. - - Parameters - ---------- - figure_thumbnail: spyder.plugins.plots.widget.figurebrowser.FigureThumbnail - The figure thumbnail to remove. - """ - - sig_save_figure_requested = Signal(object, str) - """ - This signal is emitted to request the saving of a figure thumbnail. - - Parameters - ---------- - figure_thumbnail: spyder.plugins.plots.widget.figurebrowser.FigureThumbnail - The figure thumbnail to save. - format: str - The image format to use when saving the image. One of "image/png", - "image/jpeg" and "image/svg+xml". - """ - sig_context_menu_requested = Signal(QPoint) """ This signal is emitted to request a context menu. diff --git a/spyder/plugins/variableexplorer/confpage.py b/spyder/plugins/variableexplorer/confpage.py index a2e7a8d0ecd..e40c2eefd2b 100644 --- a/spyder/plugins/variableexplorer/confpage.py +++ b/spyder/plugins/variableexplorer/confpage.py @@ -7,15 +7,17 @@ """Variable Explorer Plugin Configuration Page.""" # Third party imports -from qtpy.QtWidgets import QGroupBox, QVBoxLayout +from qtpy.QtWidgets import QGroupBox, QVBoxLayout, QLabel # Local imports from spyder.config.base import _ from spyder.api.preferences import PluginConfigPage +from spyder.plugins.variableexplorer import plotlib class VariableExplorerConfigPage(PluginConfigPage): def setup_page(self): + # Filter Group filter_group = QGroupBox(_("Filter")) filter_data = [ ('exclude_private', _("Exclude private references")), @@ -27,20 +29,40 @@ def setup_page(self): ] filter_boxes = [self.create_checkbox(text, option) for option, text in filter_data] - - display_group = QGroupBox(_("Display")) - display_data = [('minmax', _("Show arrays min/max"), '')] - display_boxes = [self.create_checkbox(text, option, tip=tip) - for option, text, tip in display_data] - filter_layout = QVBoxLayout() for box in filter_boxes: filter_layout.addWidget(box) filter_group.setLayout(filter_layout) + # Display Group + display_group = QGroupBox(_("Display")) + display_data = [("minmax", _("Show arrays min/max"), "")] + display_boxes = [ + self.create_checkbox(text, option, tip=tip) + for option, text, tip in display_data + ] display_layout = QVBoxLayout() for box in display_boxes: display_layout.addWidget(box) + plotlib_opt = self.create_combobox( + _("Plotting library:") + " ", + zip(plotlib.SUPPORTED_PLOTLIBS, plotlib.SUPPORTED_PLOTLIBS), + "plotlib", + default=plotlib.DEFAULT_PLOTLIB, + tip=_( + "Default library used for data plotting of NumPy arrays " + "(curve, histogram, image).

Regarding the " + "%varexp magic command, this option will be " + "applied the next time a console is opened." + ), + ) + display_layout.addWidget(plotlib_opt) + if not plotlib.AVAILABLE_PLOTLIBS: + msg = "%s" % plotlib.REQ_ERROR_MSG[:-1] + msg += " " + _("for enabling data plotting from Spyder IDE process.") + plotlib_msg = QLabel(msg) + plotlib_msg.setWordWrap(True) + display_layout.addWidget(plotlib_msg) display_group.setLayout(display_layout) vlayout = QVBoxLayout() diff --git a/spyder/plugins/variableexplorer/plotlib.py b/spyder/plugins/variableexplorer/plotlib.py new file mode 100644 index 00000000000..1274ffcab76 --- /dev/null +++ b/spyder/plugins/variableexplorer/plotlib.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +# +# Copyright © Spyder Project Contributors +# Licensed under the terms of the MIT License +# (see spyder/__init__.py for details) + +""" +Variable Explorer Plugin plotting library management. +""" + +# Standard library imports +import importlib + +# Local imports +from spyder.config.base import _ + + +# Defining compatible plotting libraries +SUPPORTED_PLOTLIBS = ("matplotlib", "guiqwt") + +# Default library is the first one of the list +DEFAULT_PLOTLIB = SUPPORTED_PLOTLIBS[0] + + +def is_package_installed(modname): + """Check if package is installed **without importing it** + + Note: As Spyder won't start if matplotlib has been imported too early, + we do not use `utils.programs.is_module_installed` here because + it imports module to check if it's installed. + """ + return importlib.util.find_spec(modname) is not None + + +def get_available_plotlibs(): + """Return list of available plotting libraries""" + return [name for name in SUPPORTED_PLOTLIBS if is_package_installed(name)] + + +def get_requirement_error_message(): + """Return html error message when no library is available""" + txt = ", ".join(["%s" % name for name in SUPPORTED_PLOTLIBS]) + return _("Please install a compatible plotting library (%s).") % txt + + +AVAILABLE_PLOTLIBS = get_available_plotlibs() +REQ_ERROR_MSG = get_requirement_error_message() diff --git a/spyder/pyplot.py b/spyder/pyplot.py deleted file mode 100644 index 6890e731def..00000000000 --- a/spyder/pyplot.py +++ /dev/null @@ -1,18 +0,0 @@ -# -*- coding: utf-8 -*- -# ----------------------------------------------------------------------------- -# Copyright (c) 2009- Spyder Project Contributors -# -# Distributed under the terms of the MIT License -# (see spyder/__init__.py for details) -# ----------------------------------------------------------------------------- - - -""" -Import guiqwt's pyplot module or matplotlib's pyplot. -""" - - -try: - from guiqwt.pyplot import * -except Exception: - from matplotlib.pyplot import * diff --git a/spyder/widgets/collectionseditor.py b/spyder/widgets/collectionseditor.py index 5b8e0a76a37..4fd06d4c9f7 100644 --- a/spyder/widgets/collectionseditor.py +++ b/spyder/widgets/collectionseditor.py @@ -60,6 +60,7 @@ from spyder.plugins.variableexplorer.widgets.importwizard import ImportWizard from spyder.widgets.helperwidgets import CustomSortFilterProxy from spyder.plugins.variableexplorer.widgets.basedialog import BaseDialog +from spyder.plugins.variableexplorer.plotlib import REQ_ERROR_MSG from spyder.utils.palette import SpyderPalette from spyder.utils.stylesheet import PANES_TOOLBAR_STYLESHEET @@ -1132,57 +1133,45 @@ def view_item(self): index = index.child(index.row(), 3) self.delegate.createEditor(self, None, index, object_explorer=True) - def __prepare_plot(self): - try: - import guiqwt.pyplot #analysis:ignore - return True - except: - try: - if 'matplotlib' not in sys.modules: - import matplotlib # noqa - return True - except Exception: - QMessageBox.warning(self, _("Import error"), - _("Please install matplotlib" - " or guiqwt.")) - def plot_item(self, funcname): """Plot item""" index = self.currentIndex() - if self.__prepare_plot(): - if self.proxy_model: - key = self.source_model.get_key( - self.proxy_model.mapToSource(index)) - else: - key = self.source_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)) + if self.proxy_model: + key = self.source_model.get_key( + self.proxy_model.mapToSource(index)) + else: + key = self.source_model.get_key(index) + try: + self.plot(key, funcname) + except (ValueError, TypeError) as error: + QMessageBox.critical( + self, + _( "Plot"), + _("Unable to plot data.

" + "The error message was:
%s") % str(error) + ) @Slot() def imshow_item(self): """Imshow item""" index = self.currentIndex() - if self.__prepare_plot(): - if self.proxy_model: - key = self.source_model.get_key( - self.proxy_model.mapToSource(index)) + if self.proxy_model: + key = self.source_model.get_key( + self.proxy_model.mapToSource(index)) + else: + key = self.source_model.get_key(index) + try: + if self.is_image(key): + self.show_image(key) else: - key = self.source_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)) + self.imshow(key) + except (ValueError, TypeError) as error: + QMessageBox.critical( + self, + _( "Plot"), + _("Unable to show image.

" + "The error message was:
%s") % str(error) + ) @Slot() def save_array(self): @@ -1383,21 +1372,38 @@ def oedit(self, key): oedit) oedit(data[key]) + def __get_pyplot(self): + """ + Return default plotting library `pyplot` package if available. + + The default plotting library is an option defined in the Variable + Explorer plugin scope, e.g. "maplotlib" or "guiqwt". + """ + libname = self.get_conf('plotlib', section='variable_explorer') + try: + return __import__(libname + '.pyplot', + globals(), locals(), [], 0).pyplot + except Exception: + msg = _("Unable to plot data using %s library.") % libname + QMessageBox.critical(self, _("Error"), msg + "

" + REQ_ERROR_MSG) + def plot(self, key, funcname): """Plot item""" data = self.source_model.get_data() - import spyder.pyplot as plt - plt.figure() - getattr(plt, funcname)(data[key]) - plt.show() + plt = self.__get_pyplot() + if plt is not None: + plt.figure() + getattr(plt, funcname)(data[key]) + plt.show() def imshow(self, key): """Show item's image""" data = self.source_model.get_data() - import spyder.pyplot as plt - plt.figure() - plt.imshow(data[key]) - plt.show() + plt = self.__get_pyplot() + if plt is not None: + plt.figure() + plt.imshow(data[key]) + plt.show() def show_image(self, key): """Show image (item is a PIL image)"""