From 2acea0f2723be09ff09717779edfcb64b79a3009 Mon Sep 17 00:00:00 2001 From: nstelter-slac Date: Fri, 11 Apr 2025 15:45:41 -0700 Subject: [PATCH] MNT: deal with large number of warnings when running tests on pyside6 when running run_tests.py on pyside6, we get many warnings output. avoid the "DeprecationWarning" for QMouseEvent calls by slightly modifying the call to use the new format as seen here: https://doc.qt.io/qt-6/qmouseevent.html#public-functions and then suppress "RuntimeWarning: Failed to disconnect" warnings, since these seem to just be the result of expected failed signal disconnections. we expect these failed disconnects b/c of sections like the following, where we try to disconnect the signal with all possible types. for signal_type in (int, float, str, bool, object): try: self.new_value_signal[signal_type].disconnect(channel.value_slot) ... also suppress warnings in a few other odd cases. --- pydm/data_plugins/plugin.py | 182 +++++++++++++++-------------- pydm/tests/widgets/test_base.py | 7 +- pydm/tests/widgets/test_label.py | 94 ++++++++------- pydm/tests/widgets/test_slider.py | 24 +++- pydm/widgets/archiver_time_plot.py | 8 +- 5 files changed, 176 insertions(+), 139 deletions(-) diff --git a/pydm/data_plugins/plugin.py b/pydm/data_plugins/plugin.py index 7ec6944eb..0ee1a8bf5 100644 --- a/pydm/data_plugins/plugin.py +++ b/pydm/data_plugins/plugin.py @@ -2,6 +2,7 @@ import numpy as np import weakref import threading +import warnings from typing import Optional, Callable from urllib.parse import ParseResult @@ -105,103 +106,106 @@ def remove_listener(self, channel, destroying: Optional[bool] = False) -> None: QObject is destroyed, setting this to True ensures we do not try to do the disconnection a second time. If set to False, any active signals/slots on the channel will be manually disconnected here. """ - if self._should_disconnect(channel.connection_slot, destroying): - try: - self.connection_state_signal.disconnect(channel.connection_slot) - except TypeError: - pass + with warnings.catch_warnings(): + warnings.simplefilter("ignore", category=RuntimeWarning) - if self._should_disconnect(channel.value_slot, destroying): - for signal_type in (int, float, str, bool, object): + if self._should_disconnect(channel.connection_slot, destroying): try: - self.new_value_signal[signal_type].disconnect(channel.value_slot) - # If the signal exists (always does in this case since we define it for all 'signal_type' earlier) - # but doesn't match slot, TypeError is thrown. We also don't need to catch KeyError/IndexError here, - # since those are only thrown when signal type doesn't exist. + self.connection_state_signal.disconnect(channel.connection_slot) except TypeError: pass - if self._should_disconnect(channel.severity_slot, destroying): - try: - self.new_severity_signal.disconnect(channel.severity_slot) - except (KeyError, TypeError): - pass - - if self._should_disconnect(channel.write_access_slot, destroying): - try: - self.write_access_signal.disconnect(channel.write_access_slot) - except (KeyError, TypeError): - pass - - if self._should_disconnect(channel.enum_strings_slot, destroying): - try: - self.enum_strings_signal.disconnect(channel.enum_strings_slot) - except (KeyError, TypeError): - pass - - if self._should_disconnect(channel.unit_slot, destroying): - try: - self.unit_signal.disconnect(channel.unit_slot) - except (KeyError, TypeError): - pass - - if self._should_disconnect(channel.upper_ctrl_limit_slot, destroying): - try: - self.upper_ctrl_limit_signal.disconnect(channel.upper_ctrl_limit_slot) - except (KeyError, TypeError): - pass - - if self._should_disconnect(channel.lower_ctrl_limit_slot, destroying): - try: - self.lower_ctrl_limit_signal.disconnect(channel.lower_ctrl_limit_slot) - except (KeyError, TypeError): - pass - - if self._should_disconnect(channel.upper_alarm_limit_slot, destroying): - try: - self.upper_alarm_limit_signal.disconnect(channel.upper_alarm_limit_slot) - except (KeyError, TypeError): - pass - - if self._should_disconnect(channel.lower_alarm_limit_slot, destroying): - try: - self.lower_alarm_limit_signal.disconnect(channel.lower_alarm_limit_slot) - except (KeyError, TypeError): - pass - - if self._should_disconnect(channel.upper_warning_limit_slot, destroying): - try: - self.upper_warning_limit_signal.disconnect(channel.upper_warning_limit_slot) - except (KeyError, TypeError): - pass - - if self._should_disconnect(channel.lower_warning_limit_slot, destroying): - try: - self.lower_warning_limit_signal.disconnect(channel.lower_warning_limit_slot) - except (KeyError, TypeError): - pass - - if self._should_disconnect(channel.prec_slot, destroying): - try: - self.prec_signal.disconnect(channel.prec_slot) - except (KeyError, TypeError): - pass - - if self._should_disconnect(channel.timestamp_slot, destroying): - try: - self.timestamp_signal.disconnect(channel.timestamp_slot) - except (KeyError, TypeError): - pass - - if not destroying and channel.value_signal is not None and hasattr(self, "put_value"): - for signal_type in (str, int, float, np.ndarray, dict): + if self._should_disconnect(channel.value_slot, destroying): + for signal_type in (int, float, str, bool, object): + try: + self.new_value_signal[signal_type].disconnect(channel.value_slot) + # If the signal exists (always does in this case since we define it for all 'signal_type' earlier) + # but doesn't match slot, TypeError is thrown. We also don't need to catch KeyError/IndexError here, + # since those are only thrown when signal type doesn't exist. + except TypeError: + pass + + if self._should_disconnect(channel.severity_slot, destroying): + try: + self.new_severity_signal.disconnect(channel.severity_slot) + except (KeyError, TypeError): + pass + + if self._should_disconnect(channel.write_access_slot, destroying): try: - channel.value_signal[signal_type].disconnect(self.put_value) - # When signal type can't be found, PyQt5 throws KeyError here, but PySide6 index error. - # If signal type exists but doesn't match the slot, TypeError gets thrown. - except (KeyError, IndexError, TypeError): + self.write_access_signal.disconnect(channel.write_access_slot) + except (KeyError, TypeError): pass + if self._should_disconnect(channel.enum_strings_slot, destroying): + try: + self.enum_strings_signal.disconnect(channel.enum_strings_slot) + except (KeyError, TypeError): + pass + + if self._should_disconnect(channel.unit_slot, destroying): + try: + self.unit_signal.disconnect(channel.unit_slot) + except (KeyError, TypeError): + pass + + if self._should_disconnect(channel.upper_ctrl_limit_slot, destroying): + try: + self.upper_ctrl_limit_signal.disconnect(channel.upper_ctrl_limit_slot) + except (KeyError, TypeError): + pass + + if self._should_disconnect(channel.lower_ctrl_limit_slot, destroying): + try: + self.lower_ctrl_limit_signal.disconnect(channel.lower_ctrl_limit_slot) + except (KeyError, TypeError): + pass + + if self._should_disconnect(channel.upper_alarm_limit_slot, destroying): + try: + self.upper_alarm_limit_signal.disconnect(channel.upper_alarm_limit_slot) + except (KeyError, TypeError): + pass + + if self._should_disconnect(channel.lower_alarm_limit_slot, destroying): + try: + self.lower_alarm_limit_signal.disconnect(channel.lower_alarm_limit_slot) + except (KeyError, TypeError): + pass + + if self._should_disconnect(channel.upper_warning_limit_slot, destroying): + try: + self.upper_warning_limit_signal.disconnect(channel.upper_warning_limit_slot) + except (KeyError, TypeError): + pass + + if self._should_disconnect(channel.lower_warning_limit_slot, destroying): + try: + self.lower_warning_limit_signal.disconnect(channel.lower_warning_limit_slot) + except (KeyError, TypeError): + pass + + if self._should_disconnect(channel.prec_slot, destroying): + try: + self.prec_signal.disconnect(channel.prec_slot) + except (KeyError, TypeError): + pass + + if self._should_disconnect(channel.timestamp_slot, destroying): + try: + self.timestamp_signal.disconnect(channel.timestamp_slot) + except (KeyError, TypeError): + pass + + if not destroying and channel.value_signal is not None and hasattr(self, "put_value"): + for signal_type in (str, int, float, np.ndarray, dict): + try: + channel.value_signal[signal_type].disconnect(self.put_value) + # When signal type can't be found, PyQt5 throws KeyError here, but PySide6 index error. + # If signal type exists but doesn't match the slot, TypeError gets thrown. + except (KeyError, IndexError, TypeError): + pass + self.listener_count = self.listener_count - 1 if self.listener_count < 1: self.close() diff --git a/pydm/tests/widgets/test_base.py b/pydm/tests/widgets/test_base.py index 156a78fe3..0e0af15c1 100644 --- a/pydm/tests/widgets/test_base.py +++ b/pydm/tests/widgets/test_base.py @@ -197,7 +197,12 @@ def mock_exec_(*args): monkeypatch.setattr(QMenu, "exec_", mock_exec_) mouse_event = QMouseEvent( - QMouseEvent.MouseButtonRelease, pydm_label.rect().center(), Qt.RightButton, Qt.RightButton, Qt.ShiftModifier + QMouseEvent.MouseButtonRelease, + pydm_label.rect().center(), # localPos + pydm_label.rect().center(), # globalPos + Qt.RightButton, # button + Qt.RightButton, # buttons + Qt.ShiftModifier # modifiers ) pydm_label.open_context_menu(mouse_event) assert "Context Menu displayed." in caplog.text diff --git a/pydm/tests/widgets/test_label.py b/pydm/tests/widgets/test_label.py index 21fe7004b..dcd50ba39 100644 --- a/pydm/tests/widgets/test_label.py +++ b/pydm/tests/widgets/test_label.py @@ -4,6 +4,7 @@ import pytest import numpy as np import logging +import warnings from ...utilities import is_pydm_app from ...widgets.label import PyDMLabel @@ -480,50 +481,55 @@ def test_label_connection_changes_with_alarm_and_no_channel( tooltip : str The tooltip for the widget. This can be an empty string """ - pydm_label = PyDMLabel() - qtbot.addWidget(pydm_label) - - pydm_label.alarmSensitiveContent = alarm_sensitive_content - pydm_label.alarmSensitiveBorder = alarm_sensitive_border - pydm_label.setToolTip(tooltip) - - # Do not the channel, but set the alarm severity to normal (NONE) - pydm_label.channel = None - signals.new_severity_signal.connect(pydm_label.alarmSeverityChanged) - signals.new_severity_signal.emit(PyDMWidget.ALARM_NONE) - - # Set the connection as enabled (True) - signals.connection_state_signal.connect(pydm_label.connectionStateChanged) - - blocker = qtbot.waitSignal(signals.connection_state_signal, timeout=1000) - signals.connection_state_signal.emit(True) - blocker.wait() - - # Confirm alarm severity, style, connection state, enabling state, and tooltip - assert pydm_label._alarm_state == PyDMWidget.ALARM_NONE - assert pydm_label._connected is True - assert pydm_label.toolTip() == tooltip - assert pydm_label.isEnabled() is True - - # Next, disconnect the alarm, and check for the alarm severity, style, connection state, enabling state, and - # tooltip - signals.connection_state_signal.emit(False) - blocker.wait() - assert pydm_label._alarm_state == PyDMWidget.ALARM_NONE - - assert pydm_label._connected is False - assert pydm_label.toolTip() == tooltip - assert pydm_label.isEnabled() is True - - # Finally, reconnect the alarm, and check for the same attributes - signals.connection_state_signal.emit(True) - blocker.wait() - - # Confirm alarm severity, style, connection state, enabling state, and tooltip - assert pydm_label._alarm_state == PyDMWidget.ALARM_NONE - assert pydm_label._connected is True - assert pydm_label.toolTip() == tooltip - assert pydm_label.isEnabled() is True + # Ignore warnings on pyside6: 'RuntimeWarning: Failed to disconnect ... from signal "timeout()"', which seems to just be + # some odd issue related to pytest-qt's waitSignal(). + with warnings.catch_warnings(): + warnings.simplefilter("ignore", category=RuntimeWarning) + + pydm_label = PyDMLabel() + qtbot.addWidget(pydm_label) + + pydm_label.alarmSensitiveContent = alarm_sensitive_content + pydm_label.alarmSensitiveBorder = alarm_sensitive_border + pydm_label.setToolTip(tooltip) + + # Do not the channel, but set the alarm severity to normal (NONE) + pydm_label.channel = None + signals.new_severity_signal.connect(pydm_label.alarmSeverityChanged) + signals.new_severity_signal.emit(PyDMWidget.ALARM_NONE) + + # Set the connection as enabled (True) + signals.connection_state_signal.connect(pydm_label.connectionStateChanged) + + blocker = qtbot.waitSignal(signals.connection_state_signal, timeout=1000) + signals.connection_state_signal.emit(True) + blocker.wait() + + # Confirm alarm severity, style, connection state, enabling state, and tooltip + assert pydm_label._alarm_state == PyDMWidget.ALARM_NONE + assert pydm_label._connected is True + assert pydm_label.toolTip() == tooltip + assert pydm_label.isEnabled() is True + + # Next, disconnect the alarm, and check for the alarm severity, style, connection state, enabling state, and + # tooltip + signals.connection_state_signal.emit(False) + blocker.wait() + assert pydm_label._alarm_state == PyDMWidget.ALARM_NONE + + assert pydm_label._connected is False + assert pydm_label.toolTip() == tooltip + assert pydm_label.isEnabled() is True + + # Finally, reconnect the alarm, and check for the same attributes + signals.connection_state_signal.emit(True) + blocker.wait() + + # Confirm alarm severity, style, connection state, enabling state, and tooltip + assert pydm_label._alarm_state == PyDMWidget.ALARM_NONE + assert pydm_label._connected is True + assert pydm_label.toolTip() == tooltip + assert pydm_label.isEnabled() is True # -------------------- diff --git a/pydm/tests/widgets/test_slider.py b/pydm/tests/widgets/test_slider.py index 7975f6612..d54a9099f 100644 --- a/pydm/tests/widgets/test_slider.py +++ b/pydm/tests/widgets/test_slider.py @@ -85,15 +85,33 @@ def test_mouseMoveEvent(slider_fixture, qtbot, request): initial_value = test_slider.value() - press_event = QMouseEvent(QEvent.MouseButtonPress, start_pos, Qt.LeftButton, Qt.LeftButton, Qt.NoModifier) + press_event = QMouseEvent( + QEvent.MouseButtonPress, + start_pos, # localPos + start_pos, # globalPos + Qt.LeftButton, # button + Qt.LeftButton, # buttons + Qt.NoModifier) # modifiers QApplication.postEvent(test_slider, press_event) QApplication.processEvents() - move_event = QMouseEvent(QEvent.MouseMove, end_pos, Qt.LeftButton, Qt.LeftButton, Qt.NoModifier) + move_event = QMouseEvent( + QEvent.MouseMove, + end_pos, # localPos + end_pos, # globalPos + Qt.LeftButton, # button + Qt.LeftButton, # buttons + Qt.NoModifier) # modifiers QApplication.postEvent(test_slider, move_event) QApplication.processEvents() - release_event = QMouseEvent(QEvent.MouseButtonRelease, end_pos, Qt.LeftButton, Qt.LeftButton, Qt.NoModifier) + release_event = QMouseEvent( + QEvent.MouseButtonRelease, + end_pos, # localPos + end_pos, # globalPos + Qt.LeftButton, # button + Qt.LeftButton, # buttons + Qt.NoModifier) # modifier QApplication.postEvent(test_slider, release_event) QApplication.processEvents() diff --git a/pydm/widgets/archiver_time_plot.py b/pydm/widgets/archiver_time_plot.py index d02b9eb8f..54f549b37 100644 --- a/pydm/widgets/archiver_time_plot.py +++ b/pydm/widgets/archiver_time_plot.py @@ -2,6 +2,7 @@ import re import time import numpy as np +import warnings from collections import OrderedDict from typing import List, Optional, Union from pyqtgraph import DateAxisItem, ErrorBarItem, PlotCurveItem @@ -999,8 +1000,11 @@ def cache_data(self, enable: bool): return if enable: try: - self.plotItem.sigXRangeChanged.disconnect(self.updateXAxis) - self.plotItem.sigXRangeChangedManually.disconnect(self.updateXAxis) + # Catch the warnings when sigXRangeChanged and sigXRangeChangedManually were not connected yet. + with warnings.catch_warnings(): + warnings.simplefilter("ignore", category=RuntimeWarning) + self.plotItem.sigXRangeChanged.disconnect(self.updateXAxis) + self.plotItem.sigXRangeChangedManually.disconnect(self.updateXAxis) except TypeError: pass else: