Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PR: Add new "Plotting library" option, defaulting to Matplotlib (Variable Explorer) #20075

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
Open
8 changes: 4 additions & 4 deletions external-deps/spyder-kernels/.gitrepo

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 14 additions & 6 deletions external-deps/spyder-kernels/spyder_kernels/console/start.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion spyder/config/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


# =============================================================================
Expand Down Expand Up @@ -176,7 +177,8 @@
'truncate': True,
'minmax': False,
'show_callable_attributes': True,
'show_special_attributes': False
'show_special_attributes': False,
'plotlib': DEFAULT_PLOTLIB,
}),
('debugger',
{
Expand Down
1 change: 1 addition & 0 deletions spyder/plugins/ipythonconsole/utils/kernelspec.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
29 changes: 0 additions & 29 deletions spyder/plugins/plots/widgets/figurebrowser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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()
Expand All @@ -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

Expand Down Expand Up @@ -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.
Expand Down
36 changes: 29 additions & 7 deletions spyder/plugins/variableexplorer/confpage.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")),
Expand All @@ -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).<br><br>Regarding the "
"<i>%varexp</i> 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 = "<font color=orange>%s</font>" % 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()
Expand Down
47 changes: 47 additions & 0 deletions spyder/plugins/variableexplorer/plotlib.py
Original file line number Diff line number Diff line change
@@ -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(["<b>%s</b>" % 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()
18 changes: 0 additions & 18 deletions spyder/pyplot.py

This file was deleted.

106 changes: 56 additions & 50 deletions spyder/widgets/collectionseditor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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 <b>matplotlib</b>"
" or <b>guiqwt</b>."))

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"),
_("<b>Unable to plot data.</b>"
"<br><br>Error message:<br>%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"),
_("<b>Unable to plot data.</b><br><br>"
"The error message was:<br>%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"),
_("<b>Unable to show image.</b>"
"<br><br>Error message:<br>%s"
) % str(error))
self.imshow(key)
except (ValueError, TypeError) as error:
QMessageBox.critical(
self,
_( "Plot"),
_("<b>Unable to show image.</b><br><br>"
"The error message was:<br>%s") % str(error)
)

@Slot()
def save_array(self):
Expand Down Expand Up @@ -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 <b>%s</b> library.") % libname
QMessageBox.critical(self, _("Error"), msg + "<br><br>" + 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)"""
Expand Down