Skip to content

Headless pyrpl: auto-detect Qt, fall back to stubs#1

Open
christhechris wants to merge 19 commits into
mainfrom
feat/headless-pyrpl
Open

Headless pyrpl: auto-detect Qt, fall back to stubs#1
christhechris wants to merge 19 commits into
mainfrom
feat/headless-pyrpl

Conversation

@christhechris

Copy link
Copy Markdown

Summary

  • Introduces pyrpl/_qt_compat.py: on import pyrpl, tries from qtpy import ...; if missing, substitutes minimal duck-typed stubs for the QtCore/QtWidgets surface pyrpl actually uses (QObject, Signal, QTimer, QEventLoop, QApplication, QFileDialog).
  • Redirects every from qtpy import ... in pyrpl core through the shim; guards QApplication, %gui qt, QFileDialog, and the qasync-backed event loop in async_utils behind HAS_QT. wait() gets a pure-asyncio branch for the headless case (the FBG scope.single(timeout=N) path).
  • Wraps eager widget imports across ~20 core/hardware/software files in try/except ImportError, binding widget classes to None_create_widget already logs a warning and returns None when _widget_class is None.
  • Moves qtpy, qasync, pyqtgraph out of required dependencies into optional extras (pyrpl[gui]); existing pyrpl[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-end wait() and scope-single mock integration, clear-error coverage for Pyrpl(config=None, gui=True) in headless mode. conftest.py short-circuits its autouse hardware fixture when only headless tests are collected.
  • GUI users see no behavioral change.

Spec: docs/superpowers/specs/2026-04-14-headless-pyrpl-design.md
Plan: docs/superpowers/plans/2026-04-14-headless-pyrpl.md

Test plan

  • 18/18 in pyrpl/test/test_headless.py pass under QT_QPA_PLATFORM=offscreen (Qt installed).
  • 18/18 pass in a fresh python -m venv with pip install -e . and no Qt extras (HAS_QT=False, APP=None).
  • from pyrpl import RedPitaya works in Qt-free venv.
  • Existing test_redpitaya.py collection still reports LIGHT REDPITAYA DRIVER mode (hardware path unchanged).
  • Real Red Pitaya hardware run via fbg-rp-aquistion/run-dev.sh without the QT_QPA_PLATFORM=offscreen workaround — deferred to reviewer with physical access to hardware.

🤖 Generated with Claude Code

christhechris and others added 19 commits April 14, 2026 15:25
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>
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.

1 participant