Headless pyrpl: auto-detect Qt, fall back to stubs#1
Open
christhechris wants to merge 19 commits into
Open
Conversation
Spec for making `import pyrpl` work when qtpy/PyQt5/qasync are not installed, via a new pyrpl/_qt_compat.py that auto-detects Qt and substitutes no-op stubs for the subset of QtCore/QtWidgets that pyrpl uses. GUI users see no change. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
11-task plan implementing the Qt auto-detect shim via pyrpl/_qt_compat.py: TDD'd stub classes, mechanical import swaps across core + transitively-imported software modules, conditional event-loop wiring in async_utils, and a final acceptance gate that installs pyrpl in a Qt-free venv and verifies the scope path works. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Project-local git worktrees are created under .worktrees/ during development; they must not be tracked. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Introduces pyrpl/_qt_compat.py, which auto-detects qtpy at import time and substitutes minimal duck-typed stubs when Qt is absent. Stubs cover the subset of QtCore/QtWidgets that pyrpl actually uses: QObject, Signal/SignalInstance, QTimer (backed by threading.Timer), QEventLoop, QCoreApplication, QApplication, QFileDialog. GUI-only symbols raise RuntimeError with a `pip install pyrpl[gui]` hint. Added pyrpl/test/test_headless.py (11 unit tests, TDD). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
pyrpl/test/conftest.py defines an autouse session fixture (pyrpl_session_sanity) that requires a real Red Pitaya for SSH bring-up. This made test_headless.py unrunnable without hardware, since any pytest invocation would hang on retries. Detect headless-only collections and short-circuit both hardware_session and pyrpl_session_sanity. Full test suite behavior is unchanged. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
asyncio, importlib, sys, and numpy are unused at this point in the plan. They'll be re-added by later tasks (async_utils wait tests, pyrpl.py headless-error test, scope.single() mock integration) that actually need them. Keeps ruff-F clean for the current state of the branch. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SignalLauncher continues to inherit from QtCore.QObject (real Qt when installed; stub otherwise). No behavior change in GUI mode. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Stops pyrpl from instantiating QApplication and invoking IPython's `%gui qt` magic when Qt is absent. Real-Qt behavior is unchanged. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When Qt is absent, `async_utils` now (a) skips the QApplication instantiation, (b) substitutes `asyncio.new_event_loop()` for the qasync-backed LOOP, and (c) routes `wait()` through a pure-asyncio helper that runs LOOP until completion or raises TimeoutError. Real-Qt path is untouched. Adds two integration tests that exercise the headless wait via pytest monkeypatch of `_qt_compat.HAS_QT`. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Removes the module-level 'No Qt binding found' raise (the _qt_compat shim now handles Qt-missing cases gracefully). Adds a clear RuntimeError mentioning pyrpl[gui] when a user asks for the GUI config picker in headless mode. Previously they'd see a cryptic stub QFileDialog error. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Task 8's test ended up using direct sys.modules eviction; importlib was never referenced. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Removes qtpy, qasync, pyqtgraph from the core dependencies list. Adds a new `gui` extras (qtpy+qasync+pyqtgraph+PyQt5) and extends the existing `qt-pyqt5`/`qt-pyqt6`/`qt-pyside2`/`qt-pyside6` aliases so they all pull the same common base plus their specific Qt binding. Backward compatible: `pip install pyrpl[qt-pyqt5]` still works unchanged. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Asserts that a coroutine returning a numpy array resolves through async_utils.wait() in headless mode — the exact code path that scope.single(timeout=...) exercises. No hardware required; no Qt event loop started. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
attributes.py eagerly imports 14 widget classes from pyrpl.widgets at module load. Without Qt installed, that import chain crashes (qtpy, pyqtgraph). _create_widget() already handles _widget_class=None gracefully (warns, returns None), so try/except-ing the import lets pyrpl import headlessly while preserving GUI behavior unchanged. Discovered by the headless venv acceptance gate. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
20 core/hardware/software files imported widget classes at module load, plus software_modules/loop.py imported pyqtgraph. Without Qt installed, these imports cascade-crash before pyrpl._qt_compat.HAS_QT can take effect. Mirrors the attributes.py fix from e8bc5c1: try the import, bind names to None on ImportError. _create_widget already handles the None _widget_class case gracefully (warns, returns None). GUI users see no change. Discovered by the headless venv acceptance gate (Task 11). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds three missing pieces flagged by final review: - _StubQTimer.interval() getter — used by Loop.interval property - _StubQTimer.singleShot classmethod — used by gainoptimizer.py - _StubSignalInstance.__call__ — lets signal-to-signal `connect` be a no-op instead of raising TypeError every tick. In particular, stops pid.py's `timer_ival.timeout.connect(update_ival)` from producing ~3 error log lines per second per RedPitaya in headless mode. Adds three unit tests for the new stub surface. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
pyrpl/_qt_compat.py: onimport pyrpl, triesfrom qtpy import ...; if missing, substitutes minimal duck-typed stubs for the QtCore/QtWidgets surface pyrpl actually uses (QObject,Signal,QTimer,QEventLoop,QApplication,QFileDialog).from qtpy import ...in pyrpl core through the shim; guardsQApplication,%gui qt,QFileDialog, and theqasync-backed event loop inasync_utilsbehindHAS_QT.wait()gets a pure-asyncio branch for the headless case (the FBGscope.single(timeout=N)path).try/except ImportError, binding widget classes toNone—_create_widgetalready logs a warning and returnsNonewhen_widget_class is None.qtpy,qasync,pyqtgraphout of required dependencies into optional extras (pyrpl[gui]); existingpyrpl[qt-pyqt5]/[qt-pyqt6]/[qt-pyside2]/[qt-pyside6]keep working.pyrpl/test/test_headless.py(18 tests): unit coverage for every stub class, end-to-endwait()and scope-single mock integration, clear-error coverage forPyrpl(config=None, gui=True)in headless mode.conftest.pyshort-circuits its autouse hardware fixture when only headless tests are collected.Spec:
docs/superpowers/specs/2026-04-14-headless-pyrpl-design.mdPlan:
docs/superpowers/plans/2026-04-14-headless-pyrpl.mdTest plan
pyrpl/test/test_headless.pypass underQT_QPA_PLATFORM=offscreen(Qt installed).python -m venvwithpip install -e .and no Qt extras (HAS_QT=False,APP=None).from pyrpl import RedPitayaworks in Qt-free venv.LIGHT REDPITAYA DRIVERmode (hardware path unchanged).fbg-rp-aquistion/run-dev.shwithout theQT_QPA_PLATFORM=offscreenworkaround — deferred to reviewer with physical access to hardware.🤖 Generated with Claude Code