Skip to content

Feat/pyqt6 napari07 migration - #606

Closed
Alpaca233 wants to merge 2 commits into
Cephla-Lab:masterfrom
jsschwrz:feat/pyqt6-napari07-migration
Closed

Alpaca233 wants to merge 2 commits into
Cephla-Lab:masterfrom
jsschwrz:feat/pyqt6-napari07-migration

Conversation

@Alpaca233

Copy link
Copy Markdown
Collaborator

This pull request updates the codebase for compatibility with PyQt6 and Python 3.14, addressing upstream API changes and critical OpenGL rendering issues in napari/vispy canvases when embedded in hidden tabs. The changes include environment variable updates, API adjustments, and new logic to defer OpenGL repaints until canvases are visible, preventing rendering errors and crashes. Additionally, a comprehensive compatibility checklist for Python 3.14 is added.

PyQt6 migration and compatibility:

  • All instances of setting the QT_API environment variable are updated from "pyqt5" to "pyqt6" to enable PyQt6 usage throughout the codebase (console.py, core.py, core_PDAF.py, core_displacement_measurement.py, core_usbspectrometer.py, core_volumetric_imaging.py, gui_hcs.py, single_instance.py, widgets.py). [1] [2] [3] [4] [5] [6] [7] [8] [9]
  • All usage of deprecated PyQt5 APIs (e.g., QDesktopWidget, activated[str]) is replaced with PyQt6-compatible alternatives (e.g., QApplication.primaryScreen(), textActivated) in GUI code. [1] [2] [3]

Deferred OpenGL rendering for napari/vispy canvases:

  • In widgets_mosaic.py, introduces logic to detect when the mosaic canvas is hidden and defer OpenGL refreshes until the tab becomes visible, addressing PyQt6's stricter requirements for valid framebuffers. This prevents GL errors and ensures the canvas is correctly refreshed on tab activation. [1] [2] [3] [4] [5] [6]

Performance mode and napari tab management:

  • Refactors napari tab and connection management in performance mode: only the live and mosaic widgets remain connected and active during acquisition, and deferred rendering is flushed when acquisition completes. [1] [2] [3]

Code cleanup and minor fixes:

  • Removes legacy handling for QVariant in wellplate/sample format selection, as it's no longer needed in PyQt6. [1] [2]

Documentation:

  • Adds a detailed Python 3.14 compatibility checklist, highlighting supported, blocked, and uncertain packages, and provides migration recommendations.

Spencer Schwarz and others added 2 commits July 23, 2026 01:45
WHY
---
The HCS GUI embeds napari viewers (Live, Multichannel, and the unified
Mosaic/Plate view) for image display. napari 0.7 targets PyQt6, but the app
was pinning the Qt binding to PyQt5 via `QT_API=pyqt5`. Running napari 0.7 +
vispy 0.16 under PyQt5 made the Mosaic view glitchy/broken and blocked moving
to Python 3.14 (PyQt5 has no 3.14 wheels; old napari pulled in pydantic v1,
which is also incompatible with 3.14). See "Python 3.14 Compatibility Check.md"
for the full dependency analysis behind these decisions.

This migrates the app to PyQt6 so the embedded napari 0.7 viewers render
correctly and the stack can run on Python 3.14 (napari 0.7.0, PyQt6 6.11,
numpy 2.4, pydantic v2).

WHAT CHANGED
------------
1. Qt binding flipped PyQt5 -> PyQt6.
   `os.environ["QT_API"]` is set to "pyqt6" in every entry module that
   configures it before the first qtpy import: main_hcs.py, control/gui_hcs.py,
   control/widgets.py, control/console.py, control/single_instance.py,
   control/core/core.py, control/core_volumetric_imaging.py,
   control/core_usbspectrometer.py, control/core_PDAF.py,
   control/core_displacement_measurement.py, control/widgets_usbspectrometer.py,
   scripts/run_acquisition.py (child env), and tests/control/test_single_instance.py.
   Almost all Qt access already goes through qtpy, which shims the common
   PyQt5->PyQt6 differences (unscoped enums, .exec_(), QAction/QShortcut
   re-exports), so most call sites needed no change.

2. The handful of real PyQt6 breakers that qtpy does NOT shim:
   - QDesktopWidget (removed in Qt6) -> QApplication.primaryScreen() in
     control/gui_hcs.py and control/core_volumetric_imaging.py.
   - QVariant (removed in PyQt6): dropped the now-dead isinstance(x, QVariant)
     unwrap guards in control/core/core.py and control/gui_hcs.py (PyQt6 hands
     back plain Python objects).
   - QComboBox.activated[str] overload removed in Qt6 -> use the dedicated
     `textActivated` signal in control/widgets.py.
   - tools/view_laser_af_reference_image.py: converted its direct PyQt5 imports
     to qtpy and matplotlib backend_qt5agg -> backend_qtagg (binding-agnostic).

3. Shared OpenGL context for the multiple embedded napari viewers.
   main_hcs.py now sets AA_ShareOpenGLContexts before the QApplication is
   constructed. Several vispy/OpenGL canvases in one process must share a GL
   context; without it, rendering fails with "Cannot SIZE object N ..." /
   GL_INVALID_FRAMEBUFFER_OPERATION.

4. Single Qt binding requirement + setup scripts.
   The blank-canvas rendering seen during bring-up was root-caused to PyQt5 and
   PyQt6 being installed side by side (conflicting Qt libraries break napari's
   OpenGL rendering). setup_22.04.sh and setup_cuda_22.04.sh now install exactly
   one binding: PyQt6 (+ PyQt6-Qt6, PyQt6-sip) via pip, remove any pre-existing
   PyQt5, move pyqtgraph to pip (apt's python3-pyqtgraph drags in python3-pyqt5),
   bump napari to >=0.7,<0.8, and drop the "numpy<2" pin (napari 0.7 needs
   numpy>=2). Comments explain the one-binding rule so it isn't reintroduced.

5. Mosaic view: defer GL redraws while its tab is hidden (control/widgets_mosaic.py).
   Under PyQt6, painting a napari/vispy canvas that lives on a hidden QTabWidget
   page fails because the hidden QOpenGLWidget has an incomplete framebuffer,
   which corrupts the shared GL program and cascades into repeated
   "Cannot SIZE object N because it does not exist" errors during acquisition.
   The widget now writes tile pixel data unconditionally (so on-disk saves stay
   correct) but skips the GL refresh/reset/fit calls while hidden, records a
   pending-refresh flag, and flushes it in showEvent once the tab is visible.
   This makes live-during-acquisition mosaic updates work regardless of which
   tab is in front.

TESTING
-------
- Verified against the live dev environment: Python 3.14, napari 0.7.0,
  PyQt6 6.11, numpy 2.4.4, pydantic 2.13.4, qtpy 2.4.3, vispy 0.16.1.
- qtpy resolves to PyQt6; full test suite collects cleanly (1435 tests).
- GUI/pytest-qt suites pass, including tests/control/test_unified_mosaic_widget.py
  (its fixture builds a real napari.Viewer). The pre-existing full-suite
  single-process teardown crash (napari/vispy OpenGL shutdown) is unrelated to
  the binding change; run napari-heavy test files separately.
- App launches and the Mosaic/Live/Multichannel napari views render; a full
  plate acquisition completes and the mosaic populates in both Full View and
  Plate View.

KNOWN FOLLOW-UPS (pre-existing, not introduced here)
----------------------------------------------------
- Memory: full-view mosaic of a multi-well plate builds a large in-memory
  canvas (~2GB+), which napari displays at ~4x overhead, and the end-of-run
  save copies the whole canvas; back-to-back runs can stack these. Not an
  unbounded leak (canvas is reused), but peak RAM is high and causes GUI-thread
  stalls. Candidate fixes: bound the save-snapshot copy, pre-allocate the
  full-view canvas, optionally coarser full-view render resolution.
- A napari 0.7 / vispy 0.16 layer-teardown ValueError ("list.remove(x): x not
  in list") can fire when clearing layers on a mode switch. Needs a guard.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nce at completion

WHY
---
With the Mosaic view active, rendering every tile into the napari canvas during a
multi-well acquisition stalls the GUI thread (repeated GL texture rebuilds and
canvas reallocations), so the run visibly stutters and slows as it progresses. See
"Mosaic Memory Diagnosis.md" for the full analysis.

Previously "Performance Mode" avoided this by disconnecting the mosaic/multichannel
data feeds entirely — the run stayed fast, but the mosaic was never populated (the
image data only existed in the saved files on disk).

This changes Performance Mode so the mosaic still assembles during the run but
without per-tile rendering, then renders the finished mosaic once when the run
completes — you get the speed of not rendering mid-run and still see the result.

WHAT CHANGED (control/gui_hcs.py)
---------------------------------
1. updateNapariConnections: in performance mode the mosaic's data feed
   (mosaic_tile_update -> updateTile, plate_view_init -> setPlateLayout) now stays
   CONNECTED, so its canvas keeps building during acquisition. Rendering is what gets
   suppressed: UnifiedMosaicWidget already defers all GL refresh/reset/fit work while
   its tab is hidden (see UnifiedMosaicWidget.showEvent / _pending_refresh), and
   performance mode keeps that tab hidden for the whole run. The remaining napari views
   (e.g. multichannel) are still disconnected in performance mode as before.

2. toggleAcquisitionStart: the start/finish hook now manages the heavy tabs in
   performance mode.
   - start: re-apply toggleNapariTabs() so the mosaic/multichannel tabs are hidden and
     disabled for the duration of the run (a previous run's completion may have
     re-enabled them), guaranteeing rendering stays deferred.
   - finish: re-enable the napari tabs and switch to the Mosaic view; becoming visible
     fires showEvent, which flushes the single deferred refresh — the one-shot render
     of the assembled mosaic.

TEST
----
tests/control/test_performance_mode.py (new): builds the simulated GUI with the mosaic
view forced on, enters performance mode, and asserts the mosaic feed remains connected,
the mosaic tab is disabled during the run, and is re-enabled + made current at
completion. It lives in its own module on purpose: forcing the mosaic napari.Viewer on
alongside the other full-GUI tests accumulates enough napari/vispy viewers to trigger
the known STATUS_HEAP_CORRUPTION on OpenGL teardown at process exit (documented; run
napari-heavy GUI test files separately). Split out, it and test_HighContentScreeningGui.py
each exit cleanly.

NOTES / TRADE-OFFS
------------------
- Performance mode now builds the full mosaic canvas in RAM during the run (same peak
  as normal mode), trading the old mode's lower memory for a usable final render and
  no mid-run stalls. Reducing that peak is separate follow-up work (see
  "Mosaic Memory Diagnosis.md").
- The acquisition RAM pre-check is still skipped in performance mode
  (check_ram_available_with_error_dialog(..., performance_mode=...)); since the canvas
  is now built, re-enabling that check for performance mode is a reasonable follow-up
  (left unchanged here to avoid scope creep and an existing test that asserts the skip).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR migrates the application from PyQt5 to PyQt6 (via qtpy) and updates napari/vispy embedding behavior to avoid OpenGL rendering failures when canvases live inside hidden/disabled tabs, while also adding notes toward Python 3.14 readiness.

Changes:

  • Switch QT_API selection across the codebase from pyqt5 to pyqt6, and replace deprecated PyQt5-era APIs/signals with PyQt6-compatible ones.
  • Add deferred refresh logic for the mosaic napari/vispy canvas so OpenGL repaints occur only when the canvas is visible, and adjust performance mode connection/tab behavior accordingly.
  • Update Ubuntu setup scripts for napari 0.7 / NumPy >= 2, and add a Python 3.14 compatibility checklist document.

Reviewed changes

Copilot reviewed 19 out of 19 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
software/tools/view_laser_af_reference_image.py Switch to qtpy widgets + QtAgg backend compatible with Qt6; set QT_API=pyqt6.
software/tests/control/test_single_instance.py Update child process QT binding pin to pyqt6.
software/tests/control/test_performance_mode.py Add GUI test covering performance-mode mosaic deferral + flush behavior.
software/setup_cuda_22.04.sh Remove numpy<2 pin in CUDA setup path.
software/setup_22.04.sh Avoid apt-installed PyQt5/pyqtgraph; install PyQt6 via pip; move to napari 0.7 and NumPy >= 2.
software/scripts/run_acquisition.py Launch GUI subprocess with QT_API=pyqt6.
software/Python 3.14 Compatibility Check.md Add dependency readiness checklist and migration guidance for Python 3.14.
software/main_hcs.py Set QT_API=pyqt6 and enable shared OpenGL contexts before QApplication construction.
software/control/widgets.py Set QT_API=pyqt6; update combo-box signal to textActivated.
software/control/widgets_usbspectrometer.py Set QT_API=pyqt6.
software/control/widgets_mosaic.py Defer GL refresh while hidden; flush refreshes on showEvent.
software/control/single_instance.py Set QT_API=pyqt6.
software/control/gui_hcs.py Replace deprecated screen sizing API; refactor performance-mode napari connection/tab handling and mosaic flush on completion.
software/control/core/core.py Set QT_API=pyqt6; remove legacy QVariant handling in wellplate settings.
software/control/core_volumetric_imaging.py Set QT_API=pyqt6; replace QDesktopWidget usage with primary screen sizing.
software/control/core_usbspectrometer.py Set QT_API=pyqt6.
software/control/core_PDAF.py Set QT_API=pyqt6.
software/control/core_displacement_measurement.py Set QT_API=pyqt6.
software/control/console.py Set QT_API=pyqt6.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +1843 to +1847
try:
if connection_type is not None:
signal.connect(slot, connection_type)
else:
try:
if connection_type is not None:
signal.connect(slot, connection_type)
else:
signal.connect(slot)
except TypeError:
# Connection might already exist, which is fine
pass
signal.connect(slot)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

[Claude Code] Fixed in e879d3e on #622 (this PR's head is on the jsschwrz fork, which we can't push to). updateNapariConnections now disconnects before connecting, so it is idempotent across performance-mode toggles; tests/control/test_napari_connections.py asserts exact receiver counts after repeated toggles (the old code fired updateTile 4x after three toggles). Note on the suggested alternative: OR-ing Qt.UniqueConnection into the requested type isn't possible under PyQt6 — Qt.ConnectionType is a plain Enum there, so Qt.QueuedConnection | Qt.UniqueConnection raises TypeError (it works under PyQt5, where the enum is int-based).

Comment thread software/setup_22.04.sh
pip3 install pyqtgraph qtpy pyserial pandas imageio crc==1.3.0 lxml numpy tifffile scipy pyreadline3
pip3 install opencv-python-headless opencv-contrib-python-headless
pip3 install napari==0.5.4 scikit-image dask_image ome_zarr aicsimageio basicpy pytest pytest-qt pytest-xvfb gitpython matplotlib pydantic_xml pyvisa hidapi filelock lxml_html_clean psutil mcp ndv
pip3 install "napari>=0.7,<0.8" scikit-image dask_image ome_zarr aicsimageio basicpy pytest pytest-qt pytest-xvfb gitpython matplotlib pydantic_xml pyvisa hidapi filelock lxml_html_clean psutil mcp ndv

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

[Claude Code] Fixed in b45b637 on #622 — pinned to the tested combination: napari[pyqt6]==0.7.1, PyQt6==6.11.0, PyQt6-Qt6==6.11.2, PyQt6-sip==13.12.0. The [pyqt6] extra matters beyond reproducibility: it makes pip enforce napari's Qt blocklist (napari 0.7.1 blocks PyQt6-Qt6 6.11.0/6.11.1, napari/napari#9052), and the unpinned install had put 6.11.1 on this PR's CI run.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 19 out of 19 changed files in this pull request and generated 2 comments.

Comment on lines +321 to +323
if self.mode == DisplayMode.PLATE:
self._draw_plate_boundaries()
self.resetView()
Comment on lines +72 to +74
qtbot.wait(20)
assert win.imageDisplayTabs.isTabEnabled(mosaic_idx) is True
assert win.imageDisplayTabs.currentWidget() is mosaic
@Alpaca233

Copy link
Copy Markdown
Collaborator Author

[Claude Code] Dependency check after the PyQt6 / napari 0.7 / NumPy 2 update

Ran the new setup_22.04.sh package set through the resolver (Python 3.10 / Ubuntu 22.04, same as CI) and did import + read/write round-trip tests against the exact versions this PR's CI run installed. pip resolves the set without errors, but four things break:

1. import basicpy is broken in the new environment (regression vs master) — blocking

napari 0.7.1 requires scipy>=1.14; every basicpy ≥1.2 caps scipy<1.13 (upstream: peng-lab/BaSiCPy#173, open). So pip silently falls back to basicpy 1.1.0 (May 2023), whose unbounded deps now resolve to hyperactive 5.0.4 and jax 0.6.2, and its module-level imports fail two independent ways:

  • ImportError: cannot import name 'Hyperactive' from 'hyperactive' (removed in hyperactive 5)
  • ImportError: cannot import name '_wraps' from 'jax._src.numpy.util' (basicpy 1.2.0 pins jax<=0.4.23; 0.4.38 and 0.6.2 both lack it)

Evidence: this PR's CI installed basicpy-1.1.0 hyperactive-5.0.4 jax-0.6.2 scipy-1.15.3; master's last green run installed basicpy-2.0.0 hyperactive-4.8.0 scipy-1.12.0 (2.0.0 wraps the hyperactive import in try/except, so master imports fine). control/stitcher.py:28 does from basicpy import BaSiC at module level, so it fails on import. No test imports it, so CI won't catch this.

Verified workaround: basicpy 2.0.0 runs fine on scipy 1.15.3 + numpy 2.2.6 once its metadata cap is bypassed — pip3 install --no-deps basicpy==2.0.0 && pip3 install torch-dct pooch (torch already comes from setup_cuda_22.04.sh; hyperactive is optional in 2.0.0, autotune only). Fit/transform round-trip passes.

2. .github/workflows/main.yml still pins the GUI-test step to PyQt5 — blocking

The setup script now uninstalls PyQt5, but the "Run the GUI test in its own process" step sets QT_API: pyqt5 / PYTEST_QT_API: pyqt5. Reproduced in a PyQt6-only venv: pytest dies at startup with INTERNALERROR … pytestqt/qt_compat.py set_qt_api → ModuleNotFoundError: No module named 'PyQt5'. The PR's CI run never reached that step (see 5). Switch both to pyqt6 (PySide6 isn't in the PR's resolved set, so the pin is belt-and-braces now).

3. CI installed Qt 6.11.1, which napari 0.7.1 explicitly blocks — should fix

The script installs PyQt6 PyQt6-Qt6 PyQt6-sip unpinned and then bare napari (no [pyqt6] extra), so napari's guard pyqt6-qt6!=6.11.0,!=6.11.1 (napari/napari#9052 — dock-widget and resizing bugs) never applies. This PR's CI got PyQt6-6.11.0 PyQt6-Qt6-6.11.1 ("Qt runtime 6.11.1" in the pytest header). Latest today is 6.11.2, so a fresh install dodges it by luck only. Fix: install "napari[pyqt6]>=0.7,<0.8" so napari's Qt constraints apply — and pin exact versions as Copilot suggested (e.g. napari[pyqt6]==0.7.1, PyQt6==6.11.0, PyQt6-Qt6==6.11.2).

4. ndviewer_light submodule still hard-imports PyQt5 — should fix

ndviewer_light/core.py:18-20 imports PyQt5.* and its pyproject lists pyqt5; the submodule pointer isn't bumped in this PR. With PyQt5 removed, from control import ndviewer_light → ModuleNotFoundError: No module named 'PyQt5'; NDViewerTab catches it and shows the "failed to import" placeholder, so the NDViewer tab is dead (no crash). On a machine where PyQt5 survives (apt remove fails, stray pip --user PyQt5), importing it loads both bindings in one process — exactly what this PR is trying to eliminate. Needs a submodule update to qtpy/PyQt6. Also: the new env resolves ndv 0.5.0 (master: 0.4.1) while ndviewer_light pins ndv>=0.4,<0.5 — worth checking when migrating it. (fluidics_v2's software/gui.py also imports PyQt5, but Squid doesn't import that file — FYI only.)

5. The current CI failure is a segfault, not a resolver error

Install succeeded; pytest died at 68% in test_squid_laser_engine.py::test_status_updated_signal inside qtbot.wait, with leaked threads on the stack (squid_laser_engine._tick_loop, 3× slack_notifier._worker_loop, camera/utils.stream_fn) — the same leaked-thread segfault class that's been flaky on master (being addressed separately). One run can't say whether PyQt6 changes its frequency.

Checked and clean (so nobody re-does it)

  • NumPy 2.2.6 with the aicsimageio-pinned old packages (tifffile 2023.2.28, zarr 2.15.0, lxml 4.9.4) plus ome_zarr 0.11.1, aicsimageio 4.14.0, xarray 2025.6.1, dask 2026.7.1, scikit-image 0.25.2: imports and read/write round-trips all pass (py3.10 venv, CI-resolved versions).
  • No NumPy-2-removed APIs in software/ (the np.ptp(...) calls in widgets.py are the function form, which still exists).
  • The remaining matplotlib.backends.backend_qt5agg import at widgets.py:14361 works under PyQt6 (matplotlib 3.10's shim picks the already-imported binding; canvas draw verified). Switch to backend_qtagg for consistency with the tools/ change — not a blocker.
  • libxcb-cursor0 (required by Qt ≥6.5's xcb plugin) already lands on the runner.

Comment/doc accuracy (these justify the pin removals, so worth correcting)

  • setup_22.04.sh: "napari 0.7 requires PyQt6" — napari 0.7.1 still ships a [pyqt5] extra (PyQt5>=5.15.8); PyQt6 is a choice here, not a requirement. "napari 0.7 requires numpy>=2" — its metadata says numpy>=1.24.2. Moving to NumPy 2 is fine per the checks above, but the stated reason isn't.
  • Python 3.14 Compatibility Check.md: "PyQt5 — no Python 3.14 support" — PyQt5 5.15.11 wheels are cp38-abi3 and PyQt5-sip 12.19.0 publishes cp314 wheels, so it installs on 3.14. "pydantic v1 (used by old napari)" — napari 0.5.4 declares pydantic>=1.9.0 and the current pins already resolve to pydantic 2.13.x. "napari (latest) requires PyQt6" — 0.9.0 still has the pyqt5 extra.

Both of Copilot's inline comments look valid to me; the pinning one is reinforced by item 3.

@Alpaca233

Alpaca233 commented Aug 26, 2026 •

Copy link
Copy Markdown
Collaborator Author

[Claude Code] Follow-up to the dependency check above: the review items are addressed in #622, which supersedes this PR. #622 is this branch's two commits unchanged, a merge of current master (b582a96), and three fix commits (e879d3e, b45b637, ab40220). The fixes could not land here because this PR's head branch is on the jsschwrz/Squid fork and pushes to it are rejected for us (no push permission on the fork; "allow edits from maintainers" isn't available because the PR author doesn't own the fork).

Addressed in #622: both Copilot comments (idempotent updateNapariConnections + unit test; pinned napari[pyqt6]==0.7.1 / PyQt6 6.11.0 / Qt6 6.11.2), item 2 (CI GUI step → pyqt6; this only appears once master is merged in), item 3 (Qt blocklist now enforced via the [pyqt6] extra), the leftover backend_qt5agg import, and the setup-script / Python 3.14 doc corrections.

Deliberately left alone: item 1 (basicpy) and item 4 (ndviewer_light) — both are slated for removal.

Alpaca233 added a commit that referenced this pull request Aug 27, 2026
#606's new test builds the full HCS GUI inside the main pytest process. Destroying that GUI segfaults (pre-existing destructor-order crash: QSplitter -> QTabWidget -> QFrame -> QVBoxLayout -> QGridLayout -> ~QHBoxLayout; reproducible by flushing DeferredDelete right after the test), and pytest-qt deleteLater()s the window at teardown. A later test's event-loop activity flushes that deferred delete — test_squid_laser_engine::test_status_updated_signal did, killing the run at 67% on every CI attempt for #606/#622 and locally under PyQt5 and PyQt6 alike.

Same treatment test_HighContentScreeningGui.py already gets: own step, own process, SQUID_PYTEST_HARD_EXIT. Excluding the file from the main run, the full suite completes under PyQt6 (1551 passed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jsschwrz

jsschwrz commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Closing in favour of #622, which continues this work. The branch stays on the fork if anything here is still wanted.

@Alpaca233 Alpaca233 closed this Sep 3, 2026
Alpaca233 added a commit that referenced this pull request Oct 14, 2026
#606's new test builds the full HCS GUI inside the main pytest process. Destroying that GUI segfaults (pre-existing destructor-order crash: QSplitter -> QTabWidget -> QFrame -> QVBoxLayout -> QGridLayout -> ~QHBoxLayout; reproducible by flushing DeferredDelete right after the test), and pytest-qt deleteLater()s the window at teardown. A later test's event-loop activity flushes that deferred delete — test_squid_laser_engine::test_status_updated_signal did, killing the run at 67% on every CI attempt for #606/#622 and locally under PyQt5 and PyQt6 alike.

Same treatment test_HighContentScreeningGui.py already gets: own step, own process, SQUID_PYTEST_HARD_EXIT. Excluding the file from the main run, the full suite completes under PyQt6 (1551 passed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Alpaca233 added a commit that referenced this pull request Oct 17, 2026
#606's new test builds the full HCS GUI inside the main pytest process. Destroying that GUI segfaults (pre-existing destructor-order crash: QSplitter -> QTabWidget -> QFrame -> QVBoxLayout -> QGridLayout -> ~QHBoxLayout; reproducible by flushing DeferredDelete right after the test), and pytest-qt deleteLater()s the window at teardown. A later test's event-loop activity flushes that deferred delete — test_squid_laser_engine::test_status_updated_signal did, killing the run at 67% on every CI attempt for #606/#622 and locally under PyQt5 and PyQt6 alike.

Same treatment test_HighContentScreeningGui.py already gets: own step, own process, SQUID_PYTEST_HARD_EXIT. Excluding the file from the main run, the full suite completes under PyQt6 (1551 passed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants