Skip to content

Qt6 support: all extra installs napari[qt], fix ROI drawing on PyQt6 - #406

Merged
LucaMarconato merged 3 commits into
scverse:mainfrom
jan-glx:jangleixner/qt6
Nov 10, 2026
Merged

LucaMarconato merged 3 commits into
scverse:mainfrom
jan-glx:jangleixner/qt6

Conversation

@jan-glx

@jan-glx jan-glx commented Nov 9, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #405.

napari 0.10 requires Qt >= 6.7 and no longer has a pyqt5 extra. So pip install napari-spatialdata[all] installs no Qt binding at all (pip only warns), and an env with PyQt5 fails at import napari._qt. Our CI didn't catch this because tox.ini installs PyQt5 directly.

Changes

  • setup.cfg: all = napari[pyqt5] becomes napari[pyqt6]. That pins the binding napari-spatialdata is tested with, and napari's extra keeps compatible PyQt6/Qt versions. napari[qt] would instead follow napari to whatever binding it defaults to next, possibly before this plugin's Qt code (widgets, pyqtgraph) is tested on it. README updated to match.
  • tox.ini: drop the explicit PyQt5. CI now gets Qt from the all extra, the same way users do.
  • _scatterwidgets.py (user-facing bug on Qt6): ROI drawing in the scatter widget passed event.pos() (QPoint) to mapSceneToView / sceneBoundingRect().contains. PyQt6 no longer converts QPoint to QPointF implicitly, so every press/move/release while drawing raised TypeError and no ROI was created.
    • The handlers now compute the scene position once as scene_pos = self.mapToScene(event.pos()), which is a QPointF on every binding, and use it throughout. mouseReleaseEvent is also called by scene().sigMouseClicked with a pyqtgraph MouseClickEvent (whose pos() mapToScene can't take), so its position debug log is now limited to QMouseEvent.
    • Before, the viewport position event.pos() was used as a scene position, mixed with a scene_pos variable holding the same value. That only worked because pyqtgraph's default autoPixelRange keeps scene coordinates equal to viewport pixels, so the values don't change.
    • Also fixed a debug log message that was missing its f prefix.
  • Tests:
    • test_old_annotation patched "PyQt5.QtWidgets.QMessageBox.question". It now uses mocker.patch.object(QtWidgets.QMessageBox, ...) via qtpy.
    • send_mouse_event now passes a QPointF to QMouseEvent.

Tested

Tested in a Linux env with napari 0.10.0 + PyQt6 6.12.0 (headless, conftest's QT_QPA_PLATFORM=offscreen):

  • before: ImportError in conftest with PyQt5. With PyQt6 but without the code changes: 3 failed (test_old_annotation, test_roi_from_mouse_events, test_remove_roi_double_click).
  • after, napari 0.10.0 + PyQt6: 87 passed, 12 skipped, 3 xfailed.
  • after, napari 0.9.2 + PyQt5 5.15 (QT_API=pyqt5): 87 passed, so nothing changes on the old binding.
  • pip install --dry-run -e ".[all]" resolves napari 0.10.0 + PyQt6 6.12.0 with no warnings. The pyqt6 extra exists back to napari 0.6.2, the minimum allowed version.

Found via the scverse integration tests (scverse/spatialdata-integration-testing#26).

@codecov

codecov Bot commented Nov 9, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.33333% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 71.66%. Comparing base (69105e0) to head (38986af).
⚠️ Report is 71 commits behind head on main.

Files with missing lines Patch % Lines
src/napari_spatialdata/_scatterwidgets.py 93.33% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #406      +/-   ##
==========================================
+ Coverage   63.08%   71.66%   +8.58%     
==========================================
  Files          19       19              
  Lines        2636     3399     +763     
==========================================
+ Hits         1663     2436     +773     
+ Misses        973      963      -10     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

PyQt6 no longer converts QPoint to QPointF implicitly, so ROI drawing in the scatter
widget raised TypeError in mapSceneToView / sceneBoundingRect().contains and created
no ROI. The handlers now compute the scene position once as
self.mapToScene(event.pos()) (a QPointF on every binding) instead of using the
viewport position event.pos() as a scene position (same values: pyqtgraph's
autoPixelRange keeps scene == viewport pixels). Also fix a log message missing its
f-prefix.

Tests: pass a QPointF to QMouseEvent, and patch QMessageBox.question via qtpy
instead of the PyQt5 module path.
napari 0.10 requires Qt>=6.7 and no longer has a pyqt5 extra (scverse#405), so
napari-spatialdata[all] installed no Qt binding. Pin the binding explicitly to the
one napari-spatialdata is tested with (napari pins compatible PyQt6/Qt versions),
rather than napari[qt], which would follow napari to a future binding untested here.
CI now gets Qt from the extra, like users do.
mouseReleaseEvent is also connected to scene().sigMouseClicked, which passes a
pyqtgraph MouseClickEvent. Its pos() is item-local (or fails if no item took the
click) and is a QPointF that mapToScene can't take, so the always-evaluated
f-string raised on every ordinary click.
@jan-glx
jan-glx marked this pull request as ready for review November 9, 2026 13:58

@LucaMarconato LucaMarconato left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@LucaMarconato
LucaMarconato merged commit d66941b into scverse:main Nov 10, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Installation WARNING: napari 0.10.0 does not provide the extra 'pyqt5'

3 participants