From 81ec0743b349ba4b0fd358110c6cca26e3a49a3a Mon Sep 17 00:00:00 2001 From: Carifio24 Date: Wed, 3 Jan 2024 13:33:17 +0000 Subject: [PATCH 1/4] Add Qt datetime connection. --- echo/qt/autoconnect.py | 8 +++++++- echo/qt/connect.py | 42 ++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/echo/qt/autoconnect.py b/echo/qt/autoconnect.py index e9005c006..d1fb78910 100644 --- a/echo/qt/autoconnect.py +++ b/echo/qt/autoconnect.py @@ -6,7 +6,8 @@ connect_text, connect_button, connect_combo_selection, - connect_list_selection) + connect_list_selection, + connect_datetime) __all__ = ['autoconnect_callbacks_to_qt'] @@ -20,6 +21,7 @@ HANDLERS['button'] = connect_button HANDLERS['combosel'] = connect_combo_selection HANDLERS['listsel'] = connect_list_selection +HANDLERS['datetime'] = connect_datetime def autoconnect_callbacks_to_qt(instance, widget, connect_kwargs={}): @@ -64,6 +66,10 @@ def autoconnect_callbacks_to_qt(instance, widget, connect_kwargs={}): * ``combotext``: the callback property is linked to a QComboBox based on the label of the entries in the combo box. + * ``datetime``: the callback property is linked to a QDateTimeEdit. + Note that this connection will also work for the more specific QDateEdit + and QTimeEdit widgets, as they are subclasses of QDateTimeEdit. + Applications can also define additional mappings between type and auto-linking. To do this, simply add a new entry to the ``HANDLERS`` object:: diff --git a/echo/qt/connect.py b/echo/qt/connect.py index 51c97ad5a..b4470c2e4 100644 --- a/echo/qt/connect.py +++ b/echo/qt/connect.py @@ -1,10 +1,11 @@ # The functions in this module are used to connect callback properties to Qt # widgets. +from datetime import datetime import math from qtpy import QtGui, QtWidgets -from qtpy.QtCore import Qt +from qtpy.QtCore import Qt, QDateTime import numpy as np @@ -13,7 +14,7 @@ __all__ = ['connect_checkable_button', 'connect_text', 'connect_combo_data', 'connect_combo_text', 'connect_float_text', 'connect_value', - 'connect_combo_selection', 'connect_list_selection', + 'connect_combo_selection', 'connect_list_selection', 'connect_datetime', 'BaseConnection'] @@ -580,3 +581,40 @@ def connect(self): def disconnect(self): remove_callback(self._instance, self._prop, self.update_widget) self._widget.itemSelectionChanged.disconnect(self.update_prop) + + +class connect_datetime(BaseConnection): + """ + Connect a CallbackProperty to a QDateTimeEdit. + Since QDateEdit and QTimeEdit are subclasses of QDateTimeEdit, this connection + will work for those more specific widgets as well. + """ + + def __init__(self, instance, prop, widget): + super(connect_datetime, self).__init__(instance, prop, widget) + self.connect() + + def update_prop(self): + value = np.datetime64(self._widget.dateTime().toPython()) + setattr(self._instance, self._prop, value) + + def update_widget(self, value): + if value is None: + value = np.datetime64('now') + ms = int(value.astype(np.timedelta64) / np.timedelta64(1, 'ms')) + qvalue = QDateTime.fromMSecsSinceEpoch(ms) + self._widget.setDateTime(qvalue) + + def connect(self): + add_callback(self._instance, self._prop, self.update_widget) + self._widget.dateTimeChanged.connect(self.update_prop) + self._widget.dateChanged.connect(self.update_prop) + self._widget.timeChanged.connect(self.update_prop) + self.update_widget(getattr(self._instance, self._prop)) + + def disconnect(self): + remove_callback(self._instance, self._prop, self.update_widget) + self._widget.dateTimeChanged.disconnect(self.update_prop) + self._widget.dateChanged.disconnect(self.update_prop) + self._widget.timeChanged.disconnect(self.update_prop) + From d442480adf36c7bc20fee81562ee90367a96b0f1 Mon Sep 17 00:00:00 2001 From: Carifio24 Date: Mon, 8 Jan 2024 16:26:44 -0600 Subject: [PATCH 2/4] Obtain datetime value directly from datetime64 when setting widget time. --- echo/qt/connect.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/echo/qt/connect.py b/echo/qt/connect.py index b4470c2e4..7e9d41322 100644 --- a/echo/qt/connect.py +++ b/echo/qt/connect.py @@ -1,7 +1,7 @@ # The functions in this module are used to connect callback properties to Qt # widgets. -from datetime import datetime +from datetime import date, datetime import math from qtpy import QtGui, QtWidgets @@ -601,9 +601,13 @@ def update_prop(self): def update_widget(self, value): if value is None: value = np.datetime64('now') - ms = int(value.astype(np.timedelta64) / np.timedelta64(1, 'ms')) - qvalue = QDateTime.fromMSecsSinceEpoch(ms) - self._widget.setDateTime(qvalue) + dt = value.item() + + # datetime64::item can return a date + # If this happens, create a datetime object with the time set to midnight + if isinstance(dt, date): + dt = datetime.combine(dt, datetime.min.time()) + self._widget.setDateTime(dt) def connect(self): add_callback(self._instance, self._prop, self.update_widget) From 0688ab1fe12a38a47adc1f4b6ad05206757835f4 Mon Sep 17 00:00:00 2001 From: Carifio24 Date: Wed, 10 Jan 2024 23:59:50 -0600 Subject: [PATCH 3/4] Updates for time zones. --- echo/qt/connect.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/echo/qt/connect.py b/echo/qt/connect.py index 7e9d41322..2b12a2b61 100644 --- a/echo/qt/connect.py +++ b/echo/qt/connect.py @@ -1,7 +1,7 @@ # The functions in this module are used to connect callback properties to Qt # widgets. -from datetime import date, datetime +from datetime import datetime import math from qtpy import QtGui, QtWidgets @@ -595,7 +595,9 @@ def __init__(self, instance, prop, widget): self.connect() def update_prop(self): - value = np.datetime64(self._widget.dateTime().toPython()) + qdatetime = self._widget.dateTime().toUTC() + qdatetime = qdatetime.toUTC() + value = np.datetime64(qdatetime.toPython()) setattr(self._instance, self._prop, value) def update_widget(self, value): @@ -604,10 +606,17 @@ def update_widget(self, value): dt = value.item() # datetime64::item can return a date - # If this happens, create a datetime object with the time set to midnight - if isinstance(dt, date): - dt = datetime.combine(dt, datetime.min.time()) - self._widget.setDateTime(dt) + # If this happens, we use midnight as our time + # (datetime is a subclass of date so we need to check this way) + if not isinstance(dt, datetime): + date = dt + time = datetime.min.time() + else: + date = dt.date() + time = dt.time() + + qdatetime = QDateTime(date, time, Qt.TimeSpec.UTC).toTimeSpec(self._widget.timeSpec()) + self._widget.setDateTime(qdatetime) def connect(self): add_callback(self._instance, self._prop, self.update_widget) From c6c7caa90ad480f601d50eaa94f1cf5abbf02339 Mon Sep 17 00:00:00 2001 From: Carifio24 Date: Thu, 11 Jan 2024 23:57:38 -0600 Subject: [PATCH 4/4] Add tests for datetime connection. --- echo/qt/connect.py | 2 -- echo/qt/tests/test_autoconnect.py | 19 +++++++++++++++++++ echo/qt/tests/test_connect.py | 29 ++++++++++++++++++++++++++++- 3 files changed, 47 insertions(+), 3 deletions(-) diff --git a/echo/qt/connect.py b/echo/qt/connect.py index 2b12a2b61..aa804c2ab 100644 --- a/echo/qt/connect.py +++ b/echo/qt/connect.py @@ -596,7 +596,6 @@ def __init__(self, instance, prop, widget): def update_prop(self): qdatetime = self._widget.dateTime().toUTC() - qdatetime = qdatetime.toUTC() value = np.datetime64(qdatetime.toPython()) setattr(self._instance, self._prop, value) @@ -630,4 +629,3 @@ def disconnect(self): self._widget.dateTimeChanged.disconnect(self.update_prop) self._widget.dateChanged.disconnect(self.update_prop) self._widget.timeChanged.disconnect(self.update_prop) - diff --git a/echo/qt/tests/test_autoconnect.py b/echo/qt/tests/test_autoconnect.py index ed2beaf01..691777668 100644 --- a/echo/qt/tests/test_autoconnect.py +++ b/echo/qt/tests/test_autoconnect.py @@ -1,4 +1,8 @@ +from datetime import datetime + +from numpy import datetime64 from qtpy import QtWidgets, QtGui +from qtpy.QtCore import QDateTime, Qt from echo.qt.autoconnect import autoconnect_callbacks_to_qt from echo import CallbackProperty @@ -47,6 +51,10 @@ def __init__(self, parent=None): self.bool_log.setCheckable(True) self.layout.addWidget(self.bool_log) + self.datetime_dob = QtWidgets.QDateTimeEdit(objectName="datetime_dob") + self.datetime_dob.setTimeSpec(Qt.TimeSpec.UTC) + self.layout.addWidget(self.datetime_dob) + class Person(object): planet = CallbackProperty() dataset = CallbackProperty() @@ -54,6 +62,7 @@ class Person(object): age = CallbackProperty() height = CallbackProperty() log = CallbackProperty() + dob = CallbackProperty() widget = CustomWidget() @@ -86,6 +95,11 @@ class Person(object): widget.bool_log.setChecked(True) assert person.log + dob = datetime(2000, 1, 1, 11, 52, 6) + qdob = QDateTime(dob.date(), dob.time(), Qt.TimeSpec.UTC) + widget.datetime_dob.setDateTime(qdob) + assert person.dob.item() == dob + # Check that modifying the callback properties updates the Qt widget person.planet = 'mars' @@ -106,6 +120,11 @@ class Person(object): person.log = False assert not widget.bool_log.isChecked() + dob = datetime(2010, 2, 3, 16, 22, 11) + person.dob = datetime64(dob) + qdatetime = widget.datetime_dob.dateTime().toUTC() + assert qdatetime.toPython() == person.dob.item() + def test_autoconnect_with_empty_qt_item(): diff --git a/echo/qt/tests/test_connect.py b/echo/qt/tests/test_connect.py index df3e6398f..d1b0f919e 100644 --- a/echo/qt/tests/test_connect.py +++ b/echo/qt/tests/test_connect.py @@ -1,10 +1,14 @@ +from datetime import datetime + import pytest +from numpy import datetime64 from unittest.mock import MagicMock from qtpy import QtWidgets +from qtpy.QtCore import QDateTime, Qt from echo import CallbackProperty -from echo.qt.connect import (connect_checkable_button, connect_text, +from echo.qt.connect import (connect_checkable_button, connect_datetime, connect_text, connect_combo_data, connect_combo_text, connect_float_text, connect_value, connect_button, UserDataWrapper) @@ -211,3 +215,26 @@ class Example(object): assert e.a.call_count == 0 button.clicked.emit() assert e.a.call_count == 1 + + +def test_connect_datetime(): + + class Example(object): + t = CallbackProperty() + + e = Example() + + widget = QtWidgets.QDateTimeEdit() + widget.setTimeSpec(Qt.TimeSpec.UTC) + + conn = connect_datetime(e, 't', widget) # noqa + + dt = datetime(2010, 5, 7, 13, 15, 3) + qdt = QDateTime(dt.date(), dt.time(), Qt.TimeSpec.UTC) + widget.setDateTime(qdt) + widget.dateTimeChanged.emit(qdt) + assert dt == e.t.item() + + dt = datetime(2020, 3, 4, 7, 48, 16) + e.t = datetime64(dt) + assert widget.dateTime().toUTC().toPython() == dt