fix(gui): app-level event filters tolerate PySide's stray QWidgetItem
CI / lint (pull_request) Successful in 1m9s
CI / test (3.12) (pull_request) Successful in 1m3s
CI / test (3.14) (pull_request) Successful in 1m11s
CI / test-with-beamline-plugins (pxi_bec) (pull_request) Successful in 1m14s
CI / test-with-beamline-plugins (pxii_bec) (pull_request) Successful in 1m16s
CI / test-with-coverage (pull_request) Successful in 1m40s
CI / coverage-analysis (pull_request) Successful in 3s
CI / test (3.13) (pull_request) Successful in 58s
CI / test-with-beamline-plugins (pxiii_bec) (pull_request) Successful in 1m14s
Docs build and publish / docker (push) Successful in 14s
CI / lint (push) Successful in 35s
CI / test (3.12) (push) Canceled after 42s
CI / test (3.13) (push) Canceled after 37s
CI / test (3.14) (push) Canceled after 37s
CI / test-with-beamline-plugins (pxi_bec) (push) Canceled after 32s
CI / test-with-beamline-plugins (pxii_bec) (push) Canceled after 32s
CI / test-with-beamline-plugins (pxiii_bec) (push) Canceled after 27s
CI / test-with-coverage (push) Canceled after 27s
CI / coverage-analysis (push) Canceled after 0s
Build and Publish / release (push) Successful in 26s

CI (3.12, PR 232) failed test_font_zoom_keeps_frozen_column_aligned_and_reautosizes
with a pytest-qt CALL ERROR: PySide 6.9 handed both app-level filters a
QWidgetItem instead of a QEvent during layout teardown, and event.type()
raised. The wheel guard now keys on isinstance(event, QWheelEvent) and
returns False itself instead of super().eventFilter() (which type-checks
its arguments and would raise the same way); the cursor filter ignores
anything that is not a QEvent. Regression test for each.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit was merged in pull request #232.
This commit is contained in:
2026-09-17 21:12:55 +02:00
co-authored by Claude Fable 5.1
parent d70cac45bf
commit 90f653386a
4 changed files with 38 additions and 6 deletions
+4
View File
@@ -150,6 +150,10 @@ class ClickableCursorFilter(QObject):
forbidden cursor) still win: they set it later."""
def eventFilter(self, obj, event):
# Same PySide 6.9 quirk as WheelValueGuard: an app-level filter can be
# handed a QWidgetItem instead of a QEvent during layout teardown.
if not isinstance(event, QEvent):
return False
if event.type() == QEvent.Type.Polish and isinstance(
obj, (QAbstractButton, QComboBox, QSlider)
):
+8 -3
View File
@@ -1,4 +1,4 @@
from PySide6.QtCore import QEvent, QObject, Qt
from PySide6.QtCore import QObject, Qt
from PySide6.QtGui import QWheelEvent
from PySide6.QtWidgets import (
QAbstractScrollArea,
@@ -26,7 +26,10 @@ class WheelValueGuard(QObject):
GUARDED = (QAbstractSpinBox, QSlider, QDial, QComboBox, QTabBar)
def eventFilter(self, obj, event):
if event.type() == QEvent.Type.Wheel and isinstance(obj, self.GUARDED):
# isinstance, not event.type(): PySide 6.9 sometimes hands an app-level
# filter a QWidgetItem instead of a QEvent while layouts are torn down,
# and .type() then raises (pytest-qt CALL ERROR on a random test).
if isinstance(event, QWheelEvent) and isinstance(obj, self.GUARDED):
if event.buttons() & Qt.MouseButton.RightButton:
return False # right button held: deliberate value adjustment
area = obj.parentWidget()
@@ -45,7 +48,9 @@ class WheelValueGuard(QObject):
)
QApplication.sendEvent(area.viewport(), relayed)
return True
return super().eventFilter(obj, event)
# Not super().eventFilter(): QObject's does nothing but type-checks its
# arguments, so the stray QWidgetItem would raise there instead.
return False
if __name__ == "__main__":
+9 -2
View File
@@ -3,10 +3,10 @@ from unittest.mock import MagicMock, patch
import pytest
from PySide6.QtCore import QSettings, Qt
from PySide6.QtWidgets import QApplication, QDockWidget
from PySide6.QtWidgets import QApplication, QDockWidget, QWidget, QWidgetItem
from aare.gui import styles
from aare.gui.main_window import MainWindow
from aare.gui.main_window import ClickableCursorFilter, MainWindow
from aare.gui.styles import THEME_BLUEBIRD, THEME_SUNRISE, THEME_SUNSET
@@ -683,3 +683,10 @@ def test_sample_camera_frame_paints_visible_views_and_acks(qtbot, mock_ui_state,
# No subscriber (GUI started without a sample feed): the slot must not blow up.
win.prediction_thread = None
win._on_sample_camera_frame(QImage(4, 6, QImage.Format.Format_RGB888))
def test_cursor_filter_ignores_a_non_event_argument(qapp):
# PySide 6.9 can hand an app-level filter a QWidgetItem instead of a
# QEvent during layout teardown; raising there fails a random test.
widget = QWidget()
assert ClickableCursorFilter().eventFilter(widget, QWidgetItem(widget)) is False
+17 -1
View File
@@ -5,7 +5,15 @@ deliberate right-button + wheel gesture."""
import pytest
from PySide6.QtCore import QPoint, QPointF, Qt
from PySide6.QtGui import QWheelEvent
from PySide6.QtWidgets import QApplication, QScrollArea, QSlider, QSpinBox, QVBoxLayout, QWidget
from PySide6.QtWidgets import (
QApplication,
QScrollArea,
QSlider,
QSpinBox,
QVBoxLayout,
QWidget,
QWidgetItem,
)
from aare.gui.widgets.wheel_value_guard import WheelValueGuard
@@ -72,3 +80,11 @@ def test_bare_wheel_scrolls_the_enclosing_area(guard, qtbot):
QApplication.sendEvent(spin, _wheel(Qt.MouseButton.NoButton))
assert spin.value() == 50
assert bar.value() != before
def test_a_non_event_argument_is_ignored(guard, qtbot):
# PySide 6.9 can hand an app-level filter a QWidgetItem instead of a
# QEvent during layout teardown; raising there fails a random test.
widget = QWidget()
qtbot.addWidget(widget)
assert guard.eventFilter(widget, QWidgetItem(widget)) is False