From 6d90ab4ba62a9e7db7e58e61a987ffeb8148c9dc Mon Sep 17 00:00:00 2001 From: Antti Soininen Date: Wed, 10 Mar 2021 14:27:33 +0200 Subject: [PATCH 1/5] Convert axis limit units in Qt plot options widget Previously axis limits were converted to floats regardless of the actual value type in Qt backend's plot options widget. This commit makes datetime axis limits get converted to actual datetime values which are considerably easier to edit. Also, the methods used to convert the datetime values back have different names under PySide2 and PyQt. Previously the options widget was using the methods provided by PyQt only. We now support both Qt bindings. Re matplotlib/matplotlib#19075 --- .../backends/qt_editor/_formlayout.py | 12 ++++++++++-- .../backends/qt_editor/figureoptions.py | 18 ++++++++++++------ 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/lib/matplotlib/backends/qt_editor/_formlayout.py b/lib/matplotlib/backends/qt_editor/_formlayout.py index e2b371f1652d..2f690a8e109c 100644 --- a/lib/matplotlib/backends/qt_editor/_formlayout.py +++ b/lib/matplotlib/backends/qt_editor/_formlayout.py @@ -342,9 +342,17 @@ def get(self): elif isinstance(value, Real): value = float(str(field.text())) elif isinstance(value, datetime.datetime): - value = field.dateTime().toPyDateTime() + datetime_ = field.dateTime() + if hasattr(datetime_, "toPyDateTime"): + value = datetime_.toPyDateTime() + else: + value = datetime_.toPython() elif isinstance(value, datetime.date): - value = field.date().toPyDate() + date_ = field.date() + if hasattr(date_, "toPyDate"): + value = date_.toPyDate() + else: + value = date_.toPython() else: value = eval(str(field.text())) valuelist.append(value) diff --git a/lib/matplotlib/backends/qt_editor/figureoptions.py b/lib/matplotlib/backends/qt_editor/figureoptions.py index 98cf3c610773..202e615486cc 100644 --- a/lib/matplotlib/backends/qt_editor/figureoptions.py +++ b/lib/matplotlib/backends/qt_editor/figureoptions.py @@ -10,7 +10,7 @@ from matplotlib import cbook, cm, colors as mcolors, markers, image as mimage from matplotlib.backends.qt_compat import QtGui from matplotlib.backends.qt_editor import _formlayout - +from matplotlib.dates import DateConverter, num2date LINESTYLES = {'-': 'Solid', '--': 'Dashed', @@ -33,9 +33,17 @@ def figure_edit(axes, parent=None): sep = (None, None) # separator # Get / General - # Cast to builtin floats as they have nicer reprs. - xmin, xmax = map(float, axes.get_xlim()) - ymin, ymax = map(float, axes.get_ylim()) + def convert_limits(lim, converter): + """Convert axis limits for correct input editors.""" + if isinstance(converter, DateConverter): + return map(num2date, lim) + # Cast to builtin floats as they have nicer reprs. + return map(float, lim) + + xconverter = axes.xaxis.converter + xmin, xmax = convert_limits(axes.get_xlim(), xconverter) + yconverter = axes.yaxis.converter + ymin, ymax = convert_limits(axes.get_ylim(), yconverter) general = [('Title', axes.get_title()), sep, (None, "X-Axis"), @@ -52,8 +60,6 @@ def figure_edit(axes, parent=None): ] # Save the unit data - xconverter = axes.xaxis.converter - yconverter = axes.yaxis.converter xunits = axes.xaxis.get_units() yunits = axes.yaxis.get_units() From 79f338504e2fe7ae8ef07e839941710c1935a90e Mon Sep 17 00:00:00 2001 From: Antti Soininen Date: Thu, 11 Mar 2021 12:28:45 +0200 Subject: [PATCH 2/5] Add more unit tests for Qt backend Re #19075 --- lib/matplotlib/tests/test_backend_qt.py | 39 +++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/lib/matplotlib/tests/test_backend_qt.py b/lib/matplotlib/tests/test_backend_qt.py index d0d997e1ce2e..a52a9894a645 100644 --- a/lib/matplotlib/tests/test_backend_qt.py +++ b/lib/matplotlib/tests/test_backend_qt.py @@ -1,10 +1,12 @@ import copy +from datetime import date, datetime import signal from unittest import mock import matplotlib from matplotlib import pyplot as plt from matplotlib._pylab_helpers import Gcf +from matplotlib.backends.qt_editor import _formlayout import pytest @@ -13,6 +15,7 @@ from matplotlib.backends.qt_compat import QtGui except ImportError: pytestmark = pytest.mark.skip('No usable Qt5 bindings') +from matplotlib.backends.qt_compat import QtWidgets @pytest.fixture @@ -258,6 +261,20 @@ def test_figureoptions(): fig.canvas.manager.toolbar.edit_parameters() +@pytest.mark.backend('Qt5Agg', skip_on_importerror=True) +def test_figureoptions_with_datetime_axes(): + fig, ax = plt.subplots() + xydata = [ + datetime(year=2021, month=1, day=1), + datetime(year=2021, month=2, day=1) + ] + ax.plot(xydata, xydata) + with mock.patch( + "matplotlib.backends.qt_editor._formlayout.FormDialog.exec_", + lambda self: None): + fig.canvas.manager.toolbar.edit_parameters() + + @pytest.mark.backend('Qt5Agg', skip_on_importerror=True) def test_double_resize(): # Check that resizing a figure twice keeps the same window size @@ -295,3 +312,25 @@ def crashing_callback(fig, stale): canvas = FigureCanvasQTAgg(fig) fig.stale = True assert called + + +@pytest.mark.backend('Qt5Agg', skip_on_importerror=True) +def test_form_widget_get_with_datetime_field(): + if not QtWidgets.QApplication.instance(): + QtWidgets.QApplication() + form = [("Field name", datetime(year=2021, month=3, day=11))] + widget = _formlayout.FormWidget(form) + widget.setup() + values = widget.get() + assert(values == [datetime(year=2021, month=3, day=11)]) + + +@pytest.mark.backend('Qt5Agg', skip_on_importerror=True) +def test_form_widget_get_with_date_field(): + if not QtWidgets.QApplication.instance(): + QtWidgets.QApplication() + form = [("Field name", date(year=2021, month=3, day=11))] + widget = _formlayout.FormWidget(form) + widget.setup() + values = widget.get() + assert(values == [date(year=2021, month=3, day=11)]) From 15764cfbd825a9d3f0b5d48170bab9107fea6a11 Mon Sep 17 00:00:00 2001 From: Antti Soininen Date: Thu, 11 Mar 2021 13:21:19 +0200 Subject: [PATCH 3/5] Fix unit tests on systems without Qt Re #19075 --- lib/matplotlib/tests/test_backend_qt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/tests/test_backend_qt.py b/lib/matplotlib/tests/test_backend_qt.py index a52a9894a645..b185df9e4952 100644 --- a/lib/matplotlib/tests/test_backend_qt.py +++ b/lib/matplotlib/tests/test_backend_qt.py @@ -6,7 +6,6 @@ import matplotlib from matplotlib import pyplot as plt from matplotlib._pylab_helpers import Gcf -from matplotlib.backends.qt_editor import _formlayout import pytest @@ -16,6 +15,7 @@ except ImportError: pytestmark = pytest.mark.skip('No usable Qt5 bindings') from matplotlib.backends.qt_compat import QtWidgets +from matplotlib.backends.qt_editor import _formlayout @pytest.fixture From 84c36d1d24da3a20e4453863c81f51560a961fa6 Mon Sep 17 00:00:00 2001 From: Antti Soininen Date: Thu, 11 Mar 2021 13:49:09 +0200 Subject: [PATCH 4/5] Fix unit tests on systems without Qt backend Re #19075 --- lib/matplotlib/tests/test_backend_qt.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/tests/test_backend_qt.py b/lib/matplotlib/tests/test_backend_qt.py index b185df9e4952..af8d44c637fd 100644 --- a/lib/matplotlib/tests/test_backend_qt.py +++ b/lib/matplotlib/tests/test_backend_qt.py @@ -11,11 +11,10 @@ try: - from matplotlib.backends.qt_compat import QtGui + from matplotlib.backends.qt_compat import QtGui, QtWidgets + from matplotlib.backends.qt_editor import _formlayout except ImportError: pytestmark = pytest.mark.skip('No usable Qt5 bindings') -from matplotlib.backends.qt_compat import QtWidgets -from matplotlib.backends.qt_editor import _formlayout @pytest.fixture From e347200c83f83c471070bcfd4c62e24f95b85753 Mon Sep 17 00:00:00 2001 From: Antti Soininen Date: Mon, 24 May 2021 15:35:52 +0300 Subject: [PATCH 5/5] Merge two unit tests into one. Re #19075 --- lib/matplotlib/tests/test_backend_qt.py | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/lib/matplotlib/tests/test_backend_qt.py b/lib/matplotlib/tests/test_backend_qt.py index 57f91340f20a..95ef41d97978 100644 --- a/lib/matplotlib/tests/test_backend_qt.py +++ b/lib/matplotlib/tests/test_backend_qt.py @@ -301,22 +301,17 @@ def crashing_callback(fig, stale): @pytest.mark.backend('Qt5Agg', skip_on_importerror=True) -def test_form_widget_get_with_datetime_field(): +def test_form_widget_get_with_datetime_and_date_fields(): if not QtWidgets.QApplication.instance(): QtWidgets.QApplication() - form = [("Field name", datetime(year=2021, month=3, day=11))] - widget = _formlayout.FormWidget(form) - widget.setup() - values = widget.get() - assert(values == [datetime(year=2021, month=3, day=11)]) - - -@pytest.mark.backend('Qt5Agg', skip_on_importerror=True) -def test_form_widget_get_with_date_field(): - if not QtWidgets.QApplication.instance(): - QtWidgets.QApplication() - form = [("Field name", date(year=2021, month=3, day=11))] + form = [ + ("Datetime field", datetime(year=2021, month=3, day=11)), + ("Date field", date(year=2021, month=3, day=11)) + ] widget = _formlayout.FormWidget(form) widget.setup() values = widget.get() - assert(values == [date(year=2021, month=3, day=11)]) + assert values == [ + datetime(year=2021, month=3, day=11), + date(year=2021, month=3, day=11) + ]