Developing PyReconstruct with uv¶
uv is a fast, lockfile-based Python project manager.
This document describes the uv-based development workflow, which is the
canonical developer setup. The conda workflow (dev/environment_dev.yaml)
remains supported as a parallel option, but git clone + uv sync + uv run is
the flow this project develops and releases against. uv has two advantages for
contributors:
- It provisions the correct interpreter itself (the project pins
requires-python = ">=3.11,<3.12"), so you don't need a separate conda env just to get Python 3.11. uv syncinstalls PyReconstruct into a project-local.venvand resolves against the committeduv.lock, so everyone gets the same pinned dependency set. Because the package is installed,import PyReconstructalso works with noPYTHONPATHfiddling.
pyproject.tomlis the source of truth for the uv workflow. uv reads[project.dependencies](runtime),[project.optional-dependencies].test(thetestextra, pytest), and[dependency-groups].dev(dev-only tooling). The conda env andrequirements.txtare the older, parallel mechanism.
1. Install uv¶
# Linux / macOS (standalone installer; installs to ~/.local/bin)
curl -LsSf https://astral.sh/uv/install.sh | sh
# or via pipx / Homebrew / your package manager — see
# https://docs.astral.sh/uv/getting-started/installation/
Verify: uv --version.
Linux: headless Qt system libraries¶
PySide6 6.5 needs a few system libraries even under the offscreen platform.
On a fresh Debian/Ubuntu box (these match the CI tests job):
sudo apt-get install -y --no-install-recommends \
libegl1 libgl1 libxkbcommon0 libfontconfig1 libdbus-1-3 libcairo2
A machine that already runs the conda env (pyrecon_dev) has these already.
Native Cairo, on every platform¶
libcairo2 above is not a Qt library and not Linux-only. PNG section export
(File > Export > PNG) goes through cairosvg, which is a declared Python
dependency but does not bundle Cairo: it imports cairocffi, which
dlopens the native library at import time. So uv sync succeeding tells you
nothing about whether PNG export works -- the wheel installs on a machine with
no Cairo at all, and the failure arrives only when a user exports, as
OSError: no library called "cairo-2" was found.
| Platform | What you need |
|---|---|
| Debian / Ubuntu | sudo apt-get install libcairo2 (already in the line above) |
| macOS | brew install cairo, plus DYLD_FALLBACK_LIBRARY_PATH=/opt/homebrew/lib -- ctypes.util.find_library does not search Homebrew's prefix, so an installed Cairo is still invisible without it |
| Windows | a Cairo DLL (libcairo-2.dll) on PATH; the GTK runtime installer is the usual source |
SVG export (File > Export > SVG) needs only svgwrite and has no native
requirement, so it works anywhere the Python dependencies are installed.
tests/test_export_svg_png.py asserts SVG export unconditionally and skips only
the PNG raster, naming this requirement in the skip message. CI installs
libcairo2, so the PNG assertion runs on the gate.
2. Create the environment with uv sync¶
uv sync creates .venv/ in the repo (git-ignored), installs PyReconstruct in
editable mode, and applies the committed uv.lock (re-resolving only if
pyproject.toml has drifted from the lock). What else it installs depends on
which groups/extras you select:
| Want | Command |
|---|---|
| Runtime only (run the app) | uv sync --no-default-groups |
| Runtime + test deps (run the suite) | uv sync --no-default-groups --extra test |
| Full dev env (conda-parity tooling) | uv sync |
| Everything (dev tooling + test deps) | uv sync --extra test |
uv sync with no flags installs the dev dependency group
(psycopg2-binary, funlib.show.neuroglancer) because uv treats a group named
dev as a default. That mirrors the dev tooling in the conda
environment_dev.yaml, and additionally installs PyReconstruct itself in
editable mode, which the conda env does not do.
funlib.show.neuroglanceris a dev-only tool installed from git, and the only fragile dependency here. It is not needed to run the app or the test suite. If its git build fails on your platform, or you just want a lean environment, use--no-default-groupsto skip the wholedevgroup.
3. Run the app¶
uv run PyReconstruct/run.py
uv run syncs the environment first (so you can skip a separate uv sync), then
launches the GUI. Because the project declares a console script, this also works:
uv run PyReconstruct # entry point -> PyReconstruct.cli:main
Both pull the dev group by default (the funlib git build). To launch without it:
uv run --no-default-groups PyReconstruct/run.py
Note: each
uv runre-syncs.venvto match the flags you pass it, adding or removing packages so the environment exactly matches the request. Alternating betweenuv run PyReconstruct/run.py(pulls thedevgroup) and the test command below (--no-default-groups, which drops it) will reinstall/remove thedevgroup each time. To avoid the churn, pick one environment shape (e.g.uv sync --no-default-groups --extra testonce) and pass the same flags to everyuv run.
Driving the GUI from a script (PYRECON_UNATTENDED)¶
A launch that nobody is sitting in front of — a click-test harness, a screenshot run, a computer-use agent — should set this:
PYRECON_UNATTENDED=1 uv run PyReconstruct/run.py path/to/series.jser
Opening a series is allowed to ask questions: where the images went if the
recorded src_dir no longer resolves, what the series code should be, whether
to scale an unscaled zarr. Each is a modal, and a modal raised into a window no
script will ever click is a permanent stall, not a slow dialog. The offscreen
platform is already treated as "no user" for this reason, but a scripted run is
on a real platform, so nothing Qt can observe tells it apart from a real
user's session. This variable is how the caller says so; each prompt then takes
the same deliberate no-user answer it already takes offscreen
(gui/utils/utils.py, user_is_present).
Exactly 1 enables it. Leave it unset for an ordinary launch — it suppresses
dialogs a real user wants to see.
4. Run the tests¶
The suite imports gui.main (PySide6), so it runs under the offscreen Qt
platform. No X server or xvfb is needed. The lean test environment (runtime +
pytest, no dev tooling) installs exactly what CI installs (pip install -e ".[test]"):
QT_QPA_PLATFORM=offscreen uv run --no-default-groups --extra test pytest -ra
-ra (what CI runs) surfaces xfail/xpass/skip reasons; pytest.ini already sets
addopts = -q, so a quiet run needs no extra flag. The suite is expected to be
green with a handful of documented xfails (orjson NaN/Inf JSON divergence).
pytest lives in the
testextra, not the defaultdevgroup, so a bareuv syncdoes not install it. Always pass--extra testto run the suite. pytest is constrained to the9.xline the suite is verified against (8.x is untested; 10 drops a config shim deprecated in 9.1).
5. Preview scripts (uv run --script)¶
Standalone preview scripts under dev/ carry PEP 723
inline metadata, so uv builds a throwaway environment from the script's own
header, with no project sync and no conda env:
uv run --script dev/update_dialog_preview.py
(That particular script renders the in-app update dialog and needs a real display. Run it on macOS/Windows or a Linux box with a desktop, not offscreen.)
6. The lockfile¶
uv.lock is committed: PyReconstruct ships as an application, so a pinned,
reproducible dependency set is what we want. uv sync (and uv run) apply it as
is, and uv sync --frozen errors out rather than silently re-resolving if the
lock and pyproject.toml have diverged. That is what CI and reproducible setups
should use.
Bumping dependencies is a maintainer action: edit the pin in pyproject.toml
(or not, for a plain refresh), then re-resolve and commit the new lock:
uv lock --upgrade # re-resolve everything to newest allowed, rewrite uv.lock
uv lock --upgrade-package <name> # bump just one dependency
uv lock # re-resolve after editing a pin in pyproject.toml
uv sync # apply the new lock to .venv
Commit the resulting uv.lock alongside the pyproject.toml change.
uv ↔ conda quick reference¶
| Task | conda (pyrecon_dev) |
uv |
|---|---|---|
| Create / update env | conda env create -f dev/environment_dev.yaml |
uv sync |
| Run the app | python PyReconstruct/run.py |
uv run PyReconstruct/run.py |
| Run tests | QT_QPA_PLATFORM=offscreen python -m pytest -ra |
QT_QPA_PLATFORM=offscreen uv run --no-default-groups --extra test pytest -ra |
| Preview script | python dev/update_dialog_preview.py |
uv run --script dev/update_dialog_preview.py |
The conda workflow remains fully supported. One practical difference: the conda
env installs dependencies but not PyReconstruct itself, so the conda commands
above rely on running from the repo root (or a PYTHONPATH/link_shell.sh
setup); the uv .venv installs the package, so it needs neither.