diff --git a/Framework/PythonInterface/mantid/BundlePython.cmake b/Framework/PythonInterface/mantid/BundlePython.cmake index 4f9752453ad877029cfdccc92fadb5ee0d916805..0f0ec08f764d1a82d4198e870a57ccee95bfc073 100644 --- a/Framework/PythonInterface/mantid/BundlePython.cmake +++ b/Framework/PythonInterface/mantid/BundlePython.cmake @@ -15,6 +15,11 @@ if( MSVC ) install ( DIRECTORY ${PYTHON_DIR}/Scripts DESTINATION bin PATTERN ".svn" EXCLUDE PATTERN ".git" EXCLUDE PATTERN "*_d.py" EXCLUDE ) install ( DIRECTORY ${PYTHON_DIR}/tcl DESTINATION bin PATTERN ".svn" EXCLUDE PATTERN ".git" EXCLUDE ) install ( FILES ${PYTHON_DIR}/python${PYTHON_MAJOR_VERSION}${PYTHON_MINOR_VERSION}.dll - ${PYTHON_DIR}/python${PYTHON_MAJOR_VERSION}.dll ${PYTHON_EXECUTABLE} ${PYTHONW_EXECUTABLE} + ${PYTHON_EXECUTABLE} ${PYTHONW_EXECUTABLE} DESTINATION bin ) + # Python >= 3 has an minimal API DLL too + if ( EXISTS ${PYTHON_DIR}/python${PYTHON_MAJOR_VERSION}.dll ) + install ( FILES ${PYTHON_DIR}/python${PYTHON_MAJOR_VERSION}.dll + DESTINATION bin ) + endif() endif() diff --git a/Framework/PythonInterface/mantid/plots/__init__.py b/Framework/PythonInterface/mantid/plots/__init__.py index 45c96ede2bc51e6ca0ad7c285369ec85bef3d4d0..7ce38b4fc87b82bc384a9b413339977fb768d34b 100644 --- a/Framework/PythonInterface/mantid/plots/__init__.py +++ b/Framework/PythonInterface/mantid/plots/__init__.py @@ -38,7 +38,7 @@ from mantid.plots import helperfunctions, plotfunctions, plotfunctions3D from mantid.plots.utility import autoscale_on_update from mantid.plots.helperfunctions import get_normalize_by_bin_width from mantid.plots.scales import PowerScale, SquareScale -from mantid.plots.utility import artists_hidden, MantidAxType +from mantid.plots.utility import artists_hidden, MantidAxType, legend_set_draggable from mantidqt.widgets.plotconfigdialog.legendtabwidget import LegendProperties @@ -501,7 +501,7 @@ class MantidAxes(Axes): def make_legend(self): if self.legend_ is None: - self.legend().draggable() + legend_set_draggable(self.legend(), True) else: props = LegendProperties.from_legend(self.legend_) LegendProperties.create_legend(props, self) @@ -623,7 +623,7 @@ class MantidAxes(Axes): # also remove the curve from the legend if (not self.is_empty(self)) and self.legend_ is not None: - self.legend().draggable() + legend_set_draggable(self.legend(), True) if new_kwargs: _autoscale_on = new_kwargs.pop("autoscale_on_update", self.get_autoscale_on()) @@ -748,7 +748,7 @@ class MantidAxes(Axes): container_new = [] # also remove the curve from the legend if (not self.is_empty(self)) and self.legend_ is not None: - self.legend().draggable() + legend_set_draggable(self.legend(), True) return container_new diff --git a/Framework/PythonInterface/mantid/plots/modest_image/modest_image.py b/Framework/PythonInterface/mantid/plots/modest_image/modest_image.py index 0d3544ff1f65d9cf829915eca13c02992f23f85b..88631d6fbf890291b5a782fae08bda2923c0e227 100644 --- a/Framework/PythonInterface/mantid/plots/modest_image/modest_image.py +++ b/Framework/PythonInterface/mantid/plots/modest_image/modest_image.py @@ -211,8 +211,6 @@ def imshow(axes, X, cmap=None, norm=None, aspect=None, Unlike matplotlib version, must explicitly specify axes """ - if not axes._hold: - axes.cla() if norm is not None: assert(isinstance(norm, mcolors.Normalize)) if aspect is None: diff --git a/Framework/PythonInterface/mantid/plots/utility.py b/Framework/PythonInterface/mantid/plots/utility.py index 4e02c746b12961434f6a1d815bab46bf307c7321..2e69e98908026787bb9fc91965b4fe7410910aed 100644 --- a/Framework/PythonInterface/mantid/plots/utility.py +++ b/Framework/PythonInterface/mantid/plots/utility.py @@ -11,10 +11,17 @@ from contextlib import contextmanager from matplotlib import cm from matplotlib.container import ErrorbarContainer +from matplotlib.legend import Legend from enum import Enum +if hasattr(Legend, "set_draggable"): + SET_DRAGGABLE_METHOD = "set_draggable" +else: + SET_DRAGGABLE_METHOD = "draggable" + + # Any changes here must be reflected in the definition in # the C++ MplCpp/Plot.h header. See the comment in that file # for the reason for duplication. @@ -117,6 +124,14 @@ def get_autoscale_limits(ax, axis): return locator.view_limits(axis_min, axis_max) +def legend_set_draggable(legend, state, use_blit=False, update='loc'): + """Utility function to support varying Legend api around draggable status across + the versions of matplotlib we support. Function arguments match those from matplotlib. + See matplotlib documentation for argument descriptions + """ + getattr(legend, SET_DRAGGABLE_METHOD)(state, use_blit, update) + + def zoom_axis(ax, coord, x_or_y, factor): """ Zoom in around the value 'coord' along the given axis. diff --git a/Framework/PythonInterface/test/python/mantid/plots/CMakeLists.txt b/Framework/PythonInterface/test/python/mantid/plots/CMakeLists.txt index dfc7e590e48fab793a5be6a9b9fd9423b9ebd128..e62034a5b8d1aa84d48c8f19bc094de220ebbef2 100644 --- a/Framework/PythonInterface/test/python/mantid/plots/CMakeLists.txt +++ b/Framework/PythonInterface/test/python/mantid/plots/CMakeLists.txt @@ -3,11 +3,9 @@ add_subdirectory(modest_image) # mantid.dataobjects tests set(TEST_PY_FILES - helperfunctionsTest.py - plotfunctionsTest.py - plotfunctions3DTest.py - plots__init__Test.py - ScalesTest.py) + helperfunctionsTest.py plotfunctionsTest.py plotfunctions3DTest.py + plots__init__Test.py ScalesTest.py UtilityTest.py +) check_tests_valid(${CMAKE_CURRENT_SOURCE_DIR} ${TEST_PY_FILES}) diff --git a/Framework/PythonInterface/test/python/mantid/plots/UtilityTest.py b/Framework/PythonInterface/test/python/mantid/plots/UtilityTest.py new file mode 100644 index 0000000000000000000000000000000000000000..94d4fd2a3550c3444f8c8e18712c67c786876f0a --- /dev/null +++ b/Framework/PythonInterface/test/python/mantid/plots/UtilityTest.py @@ -0,0 +1,37 @@ +# Mantid Repository : https://github.com/mantidproject/mantid +# +# Copyright © 2019 ISIS Rutherford Appleton Laboratory UKRI, +# NScD Oak Ridge National Laboratory, European Spallation Source +# & Institut Laue - Langevin +# SPDX - License - Identifier: GPL - 3.0 + +# This file is part of the mantid package +# Mantid Repository : https://github.com/mantidproject/mantid +# +# Copyright © 2018 ISIS Rutherford Appleton Laboratory UKRI, +# NScD Oak Ridge National Laboratory, European Spallation Source +# & Institut Laue - Langevin +# SPDX - License - Identifier: GPL - 3.0 + +from __future__ import absolute_import, division + +import unittest + +from mantid.plots.utility import legend_set_draggable +from mantid.py3compat.mock import create_autospec +from matplotlib.legend import Legend + + +class UtilityTest(unittest.TestCase): + + def test_legend_set_draggable(self): + legend = create_autospec(Legend) + args = (None, False, 'loc') + legend_set_draggable(legend, *args) + + if hasattr(Legend, 'set_draggable'): + legend.set_draggable.assert_called_with(*args) + else: + legend.draggable.assert_called_with(*args) + + +if __name__ == '__main__': + unittest.main() diff --git a/qt/applications/workbench/workbench/plotting/plotscriptgenerator/__init__.py b/qt/applications/workbench/workbench/plotting/plotscriptgenerator/__init__.py index 2325f12e15c02734ab029346b00bb5013a124301..9744431ef40ba85d2eb51f68d5bd8c24fa990eb4 100644 --- a/qt/applications/workbench/workbench/plotting/plotscriptgenerator/__init__.py +++ b/qt/applications/workbench/workbench/plotting/plotscriptgenerator/__init__.py @@ -11,6 +11,7 @@ from __future__ import (absolute_import, unicode_literals) from mantid.plots import MantidAxes from mantidqt.widgets.plotconfigdialog import curve_in_ax +from matplotlib.legend import Legend from workbench.plugins.editor import DEFAULT_CONTENT from workbench.plotting.plotscriptgenerator.axes import (generate_axis_limit_commands, generate_axis_label_commands, @@ -22,6 +23,10 @@ from workbench.plotting.plotscriptgenerator.utils import generate_workspace_retr FIG_VARIABLE = "fig" AXES_VARIABLE = "axes" +if hasattr(Legend, "set_draggable"): + SET_DRAGGABLE_METHOD = "set_draggable" +else: + SET_DRAGGABLE_METHOD = "draggable" def generate_script(fig, exclude_headers=False): @@ -107,7 +112,8 @@ def get_title_cmds(ax, ax_object_var): def get_legend_cmds(ax, ax_object_var): """Get command axes.set_legend""" if ax.legend_: - return ["{ax_obj}.legend().draggable()".format(ax_obj=ax_object_var)] + return ["{ax_obj}.legend().{draggable_method}()".format(ax_obj=ax_object_var, + draggable_method=SET_DRAGGABLE_METHOD)] return [] diff --git a/qt/applications/workbench/workbench/plotting/plotscriptgenerator/test/test_plotscriptgenerator.py b/qt/applications/workbench/workbench/plotting/plotscriptgenerator/test/test_plotscriptgenerator.py index 7cd871bac2d703572a5e8ae5db936421c05b7fcf..1c44cf2ad473514918066710ebc3083f49d72c8a 100644 --- a/qt/applications/workbench/workbench/plotting/plotscriptgenerator/test/test_plotscriptgenerator.py +++ b/qt/applications/workbench/workbench/plotting/plotscriptgenerator/test/test_plotscriptgenerator.py @@ -11,6 +11,7 @@ import unittest import matplotlib matplotlib.use("Agg") # noqa from matplotlib.axes import Axes +from matplotlib.legend import Legend from mantid.plots import MantidAxes from mantid.py3compat.mock import Mock, patch @@ -120,7 +121,10 @@ class PlotScriptGeneratorTest(unittest.TestCase): mock_ax = self._gen_mock_axes(legend_=True) mock_fig = Mock(get_axes=lambda: [mock_ax]) - self.assertIn('.legend().draggable()', generate_script(mock_fig)) + if hasattr(Legend, "set_draggable"): + self.assertIn('.legend().set_draggable()', generate_script(mock_fig)) + else: + self.assertIn('.legend().draggable()', generate_script(mock_fig)) @patch(GET_AUTOSCALE_LIMITS) @patch(GEN_WS_RETRIEVAL_CMDS) diff --git a/qt/applications/workbench/workbench/plotting/toolbar.py b/qt/applications/workbench/workbench/plotting/toolbar.py index 53018e5862db88511b1d4bd4b3fbfdcd2f3aaa53..d0b7a5bdd38f2068cd4944f6271beb1f84c22684 100644 --- a/qt/applications/workbench/workbench/plotting/toolbar.py +++ b/qt/applications/workbench/workbench/plotting/toolbar.py @@ -76,7 +76,6 @@ class WorkbenchNavigationToolbar(NavigationToolbar2QT): if tooltip_text is not None: a.setToolTip(tooltip_text) - self.buttons = {} # Add the x,y location widget at the right side of the toolbar # The stretch factor is 1 which means any resizing of the toolbar # will resize this label instead of the buttons. @@ -88,9 +87,6 @@ class WorkbenchNavigationToolbar(NavigationToolbar2QT): labelAction = self.addWidget(self.locLabel) labelAction.setVisible(True) - # reference holder for subplots_adjust window - self.adj_window = None - # Adjust icon size or they are too small in PyQt5 by default dpi_ratio = QtWidgets.QApplication.instance().desktop().physicalDpiX() / 100 self.setIconSize(QtCore.QSize(24 * dpi_ratio, 24 * dpi_ratio)) diff --git a/qt/python/mantidqt/widgets/codeeditor/completion.py b/qt/python/mantidqt/widgets/codeeditor/completion.py index 3da3608a69e8f901aea1d1ba1e2b86e1365a6303..d5290d19f1ff06ca625b9dd5248965869e04d27c 100644 --- a/qt/python/mantidqt/widgets/codeeditor/completion.py +++ b/qt/python/mantidqt/widgets/codeeditor/completion.py @@ -28,11 +28,13 @@ revisiting when we move to Python 3. from __future__ import (absolute_import, unicode_literals) import ast +from collections import namedtuple +import contextlib import inspect +from keyword import kwlist as python_keywords import re import sys -from keyword import kwlist as python_keywords -from collections import namedtuple +import warnings from lib2to3.pgen2.tokenize import detect_encoding from io import BytesIO @@ -48,6 +50,16 @@ from mantidqt.widgets.codeeditor.editor import CodeEditor ArgSpec = namedtuple("ArgSpec", "args varargs keywords defaults") +@contextlib.contextmanager +def _ignore_matplotlib_deprecation_warnings(): + """Context-manager to disable deprecation warnings from matplotlib while + generating the call tips""" + from matplotlib.cbook import MatplotlibDeprecationWarning + with warnings.catch_warnings(): + warnings.simplefilter("ignore", MatplotlibDeprecationWarning) + yield + + def get_builtin_argspec(builtin): """ Get the call tips for a builtin function from its docstring @@ -233,7 +245,8 @@ class CodeCompleter(object): if re.search("^#{0}import .*numpy( |,|$)", self.editor.text(), re.MULTILINE): self._add_to_completions(self._get_module_call_tips('numpy')) if re.search("^#{0}import .*pyplot( |,|$)", self.editor.text(), re.MULTILINE): - self._add_to_completions(self._get_module_call_tips('matplotlib.pyplot')) + with _ignore_matplotlib_deprecation_warnings(): + self._add_to_completions(self._get_module_call_tips('matplotlib.pyplot')) self._add_to_completions(python_keywords) self.editor.enableAutoCompletion(CodeEditor.AcsAPIs) @@ -251,7 +264,8 @@ class CodeCompleter(object): self._completions_dict[completion] = True def update_completion_api(self): - self._add_to_completions(self._get_completions_from_globals()) + with _ignore_matplotlib_deprecation_warnings(): + self._add_to_completions(self._get_completions_from_globals()) self.editor.updateCompletionAPI(self.completions) def _get_module_call_tips(self, module): diff --git a/qt/python/mantidqt/widgets/plotconfigdialog/legendtabwidget/__init__.py b/qt/python/mantidqt/widgets/plotconfigdialog/legendtabwidget/__init__.py index 72c00ca55b822bd7a2e3169f8738e04254909735..5e5ffb2aea1f1e66d05ccd6c287e3fe352bb7dba 100644 --- a/qt/python/mantidqt/widgets/plotconfigdialog/legendtabwidget/__init__.py +++ b/qt/python/mantidqt/widgets/plotconfigdialog/legendtabwidget/__init__.py @@ -8,6 +8,7 @@ from __future__ import (absolute_import, unicode_literals) +from mantid.plots.utility import legend_set_draggable from mantidqt.widgets.plotconfigdialog.colorselector import convert_color_to_hex import matplotlib from matplotlib.patches import BoxStyle @@ -157,4 +158,4 @@ class LegendProperties(dict): legend.set_visible(props['visible']) - legend.draggable(True) + legend_set_draggable(legend, True) diff --git a/qt/python/mantidqt/widgets/workspacedisplay/table/presenter.py b/qt/python/mantidqt/widgets/workspacedisplay/table/presenter.py index 00c7401a01149f9a755af4ce33dd8f0eec9951a5..1390419ee2610ca719a289936277695c5e9a7f80 100644 --- a/qt/python/mantidqt/widgets/workspacedisplay/table/presenter.py +++ b/qt/python/mantidqt/widgets/workspacedisplay/table/presenter.py @@ -12,6 +12,7 @@ from functools import partial from qtpy.QtCore import Qt from mantid.kernel import logger +from mantid.plots.utility import legend_set_draggable from mantidqt.widgets.observers.ads_observer import WorkspaceDisplayADSObserver from mantidqt.widgets.observers.observing_presenter import ObservingPresenter from mantidqt.widgets.workspacedisplay.data_copier import DataCopier @@ -358,7 +359,7 @@ class TableWorkspaceDisplay(ObservingPresenter, DataCopier): return ax.set_ylabel(column_label) - ax.legend().draggable() + legend_set_draggable(ax.legend(), True) fig.show() def _get_plot_function_from_type(self, ax, type): diff --git a/qt/widgets/common/src/Python/Sip.cpp b/qt/widgets/common/src/Python/Sip.cpp index c7a2e42958614cd69b26290653131175d6a2bb9a..3102358e82e2e0424c52a57b66e6834b7bf8c78a 100644 --- a/qt/widgets/common/src/Python/Sip.cpp +++ b/qt/widgets/common/src/Python/Sip.cpp @@ -6,6 +6,7 @@ // SPDX - License - Identifier: GPL - 3.0 + #include "MantidQtWidgets/Common/Python/Sip.h" +#include <QtGlobal> #include <sip.h> namespace MantidQt { @@ -21,30 +22,23 @@ const sipAPIDef *sipAPI() { static const sipAPIDef *sip_API = nullptr; if (sip_API) return sip_API; -#if defined(SIP_USE_PYCAPSULE) - sip_API = (const sipAPIDef *)PyCapsule_Import("sip._C_API", 0); -#else - /* Import the SIP module. */ - PyObject *sip_module = PyImport_ImportModule("sip"); - if (sip_module == NULL) - throw std::runtime_error("sip_api() - Error importing sip module"); - - /* Get the module's dictionary. */ - PyObject *sip_module_dict = PyModule_GetDict(sip_module); - - /* Get the "_C_API" attribute. */ - PyObject *c_api = PyDict_GetItemString(sip_module_dict, "_C_API"); - if (c_api == NULL) - throw std::runtime_error( - "sip_api() - Unable to find _C_API attribute in sip dictionary"); - /* Sanity check that it is the right type. */ - if (!PyCObject_Check(c_api)) - throw std::runtime_error("sip_api() - _C_API type is not a CObject"); - - /* Get the actual pointer from the object. */ - sip_API = (const sipAPIDef *)PyCObject_AsVoidPtr(c_api); + // Some configs have a private sip module inside PyQt. Try this first +#if QT_VERSION < QT_VERSION_CHECK(5, 0, 0) + sip_API = (const sipAPIDef *)PyCapsule_Import("PyQt4.sip._C_API", 0); +#elif QT_VERSION >= QT_VERSION_CHECK(5, 0, 0) && \ + QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + sip_API = (const sipAPIDef *)PyCapsule_Import("PyQt5.sip._C_API", 0); +#else +#error "Unknown sip module for Qt >= 6" #endif + // Try plain sip module + if (!sip_API) { + PyErr_Clear(); + sip_API = (const sipAPIDef *)PyCapsule_Import("sip._C_API", 0); + } + + assert(sip_API); return sip_API; } } // namespace Detail @@ -52,4 +46,4 @@ const sipAPIDef *sipAPI() { } // namespace Python } // namespace Common } // namespace Widgets -} // namespace MantidQt \ No newline at end of file +} // namespace MantidQt diff --git a/scripts/PyChop/PyChopGui.py b/scripts/PyChop/PyChopGui.py index 5cfe88870ddd7fc7a87a134e9beeb9adc377ff6f..1e052cfadf6c110124342438008993a44788acdd 100755 --- a/scripts/PyChop/PyChopGui.py +++ b/scripts/PyChop/PyChopGui.py @@ -27,6 +27,7 @@ from qtpy.QtCore import (QEventLoop, Qt) # noqa from qtpy.QtWidgets import (QAction, QCheckBox, QComboBox, QDialog, QFileDialog, QGridLayout, QHBoxLayout, QMenu, QLabel, QLineEdit, QMainWindow, QMessageBox, QPushButton, QSizePolicy, QSpacerItem, QTabWidget, QTextEdit, QVBoxLayout, QWidget) # noqa +from mantid.plots.utility import legend_set_draggable from mantidqt.MPLwidgets import FigureCanvasQTAgg as FigureCanvas from mantidqt.MPLwidgets import NavigationToolbar2QT as NavigationToolbar import matplotlib @@ -280,7 +281,7 @@ class PyChopGui(QMainWindow): self.plot_qe(ei, label_text, overplot) self.resaxes_xlim = max(ei, self.resaxes_xlim) self.resaxes.set_xlim([0, self.resaxes_xlim]) - self.resaxes.legend().draggable() + legend_set_draggable(self.resaxes.legend(), True) self.resaxes.set_xlabel('Energy Transfer (meV)') self.resaxes.set_ylabel(r'$\Delta$E (meV FWHM)') self.rescanvas.draw() @@ -299,7 +300,7 @@ class PyChopGui(QMainWindow): line, = self.qeaxes.plot(np.hstack(q2), np.concatenate((np.flipud(en), en)).tolist() * len(self.engine.detector.tthlims)) line.set_label(label_text) self.qeaxes.set_xlim([0, self.qeaxes_xlim]) - self.qeaxes.legend().draggable() + legend_set_draggable(self.qesaxes.legend(), True) self.qeaxes.set_xlabel(r'$|Q| (\mathrm{\AA}^{-1})$') self.qeaxes.set_ylabel('Energy Transfer (meV)') self.qecanvas.draw() @@ -371,7 +372,7 @@ class PyChopGui(QMainWindow): self.flxaxes1.set_xlabel('Incident Energy (meV)') self.flxaxes2.set_ylabel('Elastic Resolution FWHM (meV)') lg = self.flxaxes2.legend() - lg.draggable() + legend_set_draggable(lg, True) self.flxcanvas.draw() def update_slider(self, val=None): @@ -438,7 +439,7 @@ class PyChopGui(QMainWindow): line, = self.frqaxes2.plot(freqs, elres, 'o-') line.set_label('%s "%s" Ei = %5.3f meV' % (inst, chop, ei)) lg = self.frqaxes2.legend() - lg.draggable() + legend_set_draggable(lg, True) self.frqaxes2.set_xlim([0, np.max(freqs)]) self.frqcanvas.draw()