spacr.doctor

spacr-doctor — diagnose a spaCR installation and say how to fix it.

One command, one line per check, and for every line that is not PASS a concrete command the user can copy and run. A diagnostic that says “GPU not available” without saying what to do about it is not a diagnostic, so Result makes fix a first-class field rather than an optional afterthought, and format_report() always prints it.

The checks exist because these failures actually happened:

  • A stale editable install. This repository is checked out more than once on the same machine, and an editable install points at exactly one of them. Editing checkout A while import spacr resolves to checkout B costs hours before anyone thinks to print spacr.__file__. check_running_checkout() is the single most valuable function in this module.

  • A broken GUI dependency install. PySide6 ships with the core package, but a missing or incomplete wheel can still make the spacr command fail on import. spacr.qt has a friendly path for that; check_qt_extra() reuses its logic so the two cannot drift.

  • A GPU that is present but unusable. A CPU-only torch build on a machine with an NVIDIA card, or a driver older than the CUDA runtime torch was built against, both present as “cuda not available” and have entirely different fixes.

  • Cellpose version drift. spaCR migrated to the Cellpose 4.x / SAM API. Cellpose 3 lingering in an environment breaks at the first CellposeModel call, deep inside a run that has already spent an hour on masks.

  • A project database that is corrupt, locked, or from a newer spaCR.

  • Settings whose combination cannot work, which spacr.validate.validate_settings() already knows how to name.

Design rules, all of them load-bearing:

  • Every check is an independent module-level function taking a Context and returning a Result (or a sequence of them), so each one is callable and assertable on its own.

  • run_checks() wraps every call, so a check that raises becomes an ERROR row rather than taking down the report. A diagnostic tool that crashes while diagnosing is worse than no diagnostic tool.

  • Nothing heavy is imported at module scope. spacr-doctor --help must not pay for torch.

Classes

Context

Everything the checks are allowed to know about the invocation.

Result

One check's verdict.

Functions

build_parser(→ argparse.ArgumentParser)

Return the spacr-doctor argument parser.

check_cellpose(→ Result)

The installed Cellpose is the 4.x / SAM API spaCR calls.

check_command_on_path(→ Result)

The spacr you type belongs to the Python you are running.

check_conflicting_distributions(→ Result)

Exactly one metadata directory claims to be spaCR.

check_console_scripts(→ Result)

Every installed spacr-* command points at a module that exists.

check_core_dependencies(→ Result)

Every core dependency imports.

check_database(→ Union[Result, List[Result]])

A project database is readable, intact, unlocked, and the right schema.

check_declared_pins(→ Result)

A checkout's environment.yaml does not contradict its setup.py.

check_display(→ Result)

A GUI can actually open a window here.

check_duplicate_installs(→ Result)

Exactly one importable spacr package directory exists.

check_gpu(→ Result)

CUDA is not merely reported as present but is actually usable.

check_installer_backend(→ Result)

Report the accelerator choice recorded by the desktop installer.

check_optional_extras(→ Result)

No optional extra is half-installed.

check_python(→ Result)

The running interpreter is one spaCR supports.

check_qt_extra(→ Result)

The GUI dependencies import in this interpreter.

check_running_checkout(→ Result)

The checkout you are standing in is the one that actually runs.

check_settings(→ Union[Result, List[Result]])

A settings file names a combination that can actually run.

check_spacr_package(→ Result)

import spacr works, and reports where from.

check_torch(→ Result)

torch imports, and says whether it was built with CUDA at all.

exit_code(→ int)

0 when the installation is healthy, 1 otherwise.

format_report(→ str)

Render the rows as the text the command prints.

main(→ int)

Run every check, print the report, and return a shell exit code.

run_checks(→ List[Result])

Run the selected checks and return their rows.

summarize(→ Dict[str, int])

Count rows by verdict, always returning every key.

Module Contents

class spacr.doctor.Context[source]

Everything the checks are allowed to know about the invocation.

Parameters:
  • checkout – the directory the user believes they are editing. Defaults to the current working directory, which is the whole point: “am I running the code I am looking at” is a question about here.

  • db – optional project database to inspect.

  • settings – optional settings file (csv or json) to validate.

  • app – app key the settings file is for (mask, measure, …).

  • probe_gpu – allocate a tensor on the GPU to prove it really works. A driver/runtime mismatch is invisible until something is allocated.

class spacr.doctor.Result[source]

One check’s verdict.

Parameters:
  • check – short label shown in the left column of the report.

  • status – one of PASS, WARN, FAIL, ERROR, SKIP.

  • message – what was found, in the user’s terms.

  • fix – a command or action the user can actually carry out. Required in spirit for every non-PASS row; format_report() prints it verbatim, including newlines.

  • details – supporting facts worth showing but not worth a verdict.

property is_failure: bool[source]

True when this row should make the command exit non-zero.

spacr.doctor.build_parser() → argparse.ArgumentParser[source]

Return the spacr-doctor argument parser.

spacr.doctor.check_cellpose(ctx: Context) → Result[source]

The installed Cellpose is the 4.x / SAM API spaCR calls.

Parameters:

ctx – not read. The version bound comes from spaCR’s own installed metadata, falling back to FALLBACK_CELLPOSE_SPECIFIER when that cannot be read, and the verdict does not rest on the version string alone: the API itself is probed for models.CellposeModel, for the absence of the 3.x models.Cellpose wrapper, and for cpsam in MODEL_NAMES. A version string too odd to compare downgrades a finding to WARN rather than guessing.

spacr.doctor.check_command_on_path(ctx: Context) → Result[source]

The spacr you type belongs to the Python you are running.

A second environment earlier on PATH is the other half of “which spacr am I actually running”: the import can be right while the command is not.

Parameters:

ctx – not read. The comparison is between PATH and sys.prefix as this process sees them, so the row describes the shell that launched it: activate a different environment and the answer legitimately changes. A command found outside sys.prefix is a FAIL; no spacr command anywhere on PATH is only a WARN, since python -m spacr.doctor still works.

spacr.doctor.check_conflicting_distributions(ctx: Context) → Result[source]

Exactly one metadata directory claims to be spaCR.

Two shapes of the same problem. spacr and spacr-nightly installed together share one package directory and overwrite each other’s files. A leftover spacr.egg-info inside a checkout shadows the real install whenever the checkout is on sys.path, so version, dependency list and console-script table are all read from stale metadata.

Parameters:

ctx – not read. Every installed metadata directory on sys.path is enumerated, so — like check_duplicate_installs() — the row can change with the directory the process was launched from and not with ctx.checkout. Two distinct distribution names (SPACR_DISTRIBUTION_NAMES) are a FAIL; two copies of the same name, typically a stale spacr.egg-info, are only a WARN.

spacr.doctor.check_console_scripts(ctx: Context) → Result[source]

Every installed spacr-* command points at a module that exists.

sim=spacr.app_sim:gui_sim outlived the file it named, so the installed sim command died with ImportError. A partially upgraded install reproduces that for any command.

Parameters:

ctx – not read. The entry points come from the installed distribution’s metadata rather than from ctx.checkout’s setup.py, because it is the installed table that pip turned into scripts on PATH. Only the module half of module:function is resolved, so a command whose target function was renamed still passes here and fails when run.

spacr.doctor.check_core_dependencies(ctx: Context) → Result[source]

Every core dependency imports.

Parameters:

ctx – not read. The list is the fixed CORE_MODULES table, and each entry is genuinely imported rather than merely looked up in the installed metadata — a distribution present but broken (a torch whose shared libraries will not load, say) has to fail here rather than at hour one of a run. That makes this the slowest of the dependency checks.

spacr.doctor.check_database(ctx: Context) → Result | List[Result][source]

A project database is readable, intact, unlocked, and the right schema.

Parameters:

ctx – only ctx.db is read. None — the usual case — makes the whole check one SKIP row rather than a failure, since most invocations have no project to point at. It must name the SQLite file itself, which spaCR writes to <src>/measurements/measurements.db, not the plate directory; a path that is not a file is a FAIL. The integrity and schema rows open it read-only, but the locking row briefly takes a real write lock, so pointing this at a database a run or an open GUI is currently writing to reports that lock — which is the intended answer, not a false alarm.

spacr.doctor.check_declared_pins(ctx: Context) → Result[source]

A checkout’s environment.yaml does not contradict its setup.py.

Only meaningful inside a source checkout, and it is there that it matters: conda env create -f environment.yaml is how a new user builds an environment, so a pin in that file that setup.py forbids produces an install that is broken before anyone runs anything.

Parameters:

ctx – only ctx.checkout is read, and only to walk up to the enclosing checkout root the way check_running_checkout() does. Everything after that is read from files on disk — setup.py and environment.yaml in that root — never from the installed environment, so this row says nothing about a wheel install and is a SKIP whenever the pair is absent. Point ctx.checkout at another clone to cross-check that clone instead of the running one.

spacr.doctor.check_display(ctx: Context) → Result[source]

A GUI can actually open a window here.

Parameters:

ctx – not read. The inputs are sys.platform and this process’s QT_QPA_PLATFORM, DISPLAY and WAYLAND_DISPLAY environment variables, so the row follows the environment rather than anything the caller passes. Only Linux is examined; every other platform passes outright, as does Linux with QT_QPA_PLATFORM set to a deliberately headless value. A missing display is a WARN, never a FAIL, because the pipelines run headless.

spacr.doctor.check_duplicate_installs(ctx: Context) → Result[source]

Exactly one importable spacr package directory exists.

Parameters:

ctx – not read, and deliberately so: the candidates come from the import machinery and sys.path of the running process, which is what an actual import spacr consults. That makes the row depend on the directory this process was launched from — an empty sys.path entry means the current directory — rather than on ctx.checkout.

spacr.doctor.check_gpu(ctx: Context) → Result[source]

CUDA is not merely reported as present but is actually usable.

Parameters:

ctx – probe_gpu=True permits explicit CUDA initialization and an 8x8 tensor allocation/multiplication probe. False reports metadata without initialization, device-name or dtype-allocation probes; it cannot prove that a reported device would accept a tensor. Explicitly hidden CUDA devices are reported as skipped, while other backends remain eligible. A forced CPU selection is also reported as skipped.

spacr.doctor.check_installer_backend(ctx: Context) → Result[source]

Report the accelerator choice recorded by the desktop installer.

spacr.doctor.check_optional_extras(ctx: Context) → Result[source]

No optional extra is half-installed.

A missing extra is fine and expected. An extra with some of its distributions present and others not is a resolve that went wrong, and it fails at the moment the feature is used rather than at install time.

Parameters:

ctx – not read; the extras and the distributions in each come from OPTIONAL_EXTRAS. Presence is decided from installed metadata rather than by importing, which keeps the check cheap but means an extra that is installed and broken still counts as present — the opposite trade-off from check_core_dependencies().

spacr.doctor.check_python(ctx: Context) → Result[source]

The running interpreter is one spaCR supports.

Parameters:

ctx – not read. The verdict comes from sys.version_info and the Requires-Python field of the installed metadata, falling back to FALLBACK_REQUIRES_PYTHON when spaCR is not installed at all. Accepted only so every check shares one signature, so any Context — including a default-constructed one — gives the same row.

spacr.doctor.check_qt_extra(ctx: Context) → Result[source]

The GUI dependencies import in this interpreter.

Reuses spacr.qt’s own diagnosis — _missing_qt_extra and _QT_MISSING_MESSAGE — rather than restating which distributions the GUI needs. The legacy qt extra report key stays stable for consumers.

Parameters:

ctx – not read. The check imports the real GUI entry point, which means it is the one check that pays for importing PySide6, and it reports what that import did in this interpreter. It says nothing about whether a window can be opened — see check_display() for that half.

spacr.doctor.check_running_checkout(ctx: Context) → Result[source]

The checkout you are standing in is the one that actually runs.

The failure this exists for: two clones of spaCR on one machine, an editable install pointing at the first, and a developer editing the second. Every test passes, every edit does nothing, and nothing in the output says so.

Parameters:

ctx – only ctx.checkout is read, and it need not be the checkout root: the check walks up from it for the first directory holding both spacr/__init__.py and a setup.py or pyproject.toml, so any subdirectory of a clone works. When no such directory is found above it — the usual case when spaCR is installed from a wheel — the row falls back to comparing the editable-install target recorded by pip with what import spacr actually resolves to, and passes if there is nothing to contradict.

spacr.doctor.check_settings(ctx: Context) → Result | List[Result][source]

A settings file names a combination that can actually run.

Delegates to spacr.validate.validate_settings(), which already knows every combination this project has seen fail — normalize=True with measure, a crop_mode naming an object with no mask dimension, a mask run with all four object channels unset.

Parameters:

ctx – ctx.settings and ctx.app are both read, and both are needed. ctx.settings is a .csv or .json settings file; None is a SKIP and a path that is not a file is a FAIL. ctx.app selects which pipeline’s rules apply (mask, measure, classify, …); an empty string is a WARN instead of a guess, because the same file is valid for one app and invalid for another, and an app name that spacr.validate.validate_settings() does not recognise runs only the generic checks. Unlike the other checks this one can return many rows — one per problem found.

spacr.doctor.check_spacr_package(ctx: Context) → Result[source]

import spacr works, and reports where from.

Parameters:

ctx – not read. The row describes the interpreter this process is running in — it imports spacr and reports __file__ and __version__ — so it answers “does spaCR import” and never “is it the copy in ctx.checkout”; that second question belongs to check_running_checkout().

spacr.doctor.check_torch(ctx: Context) → Result[source]

torch imports, and says whether it was built with CUDA at all.

Parameters:

ctx – not read — in particular ctx.probe_gpu belongs to check_gpu(), not here. This row only imports torch and reports torch.version.cuda, and a CPU-only build still passes: whether that build is wrong for this machine is a question about the driver, which is why it is answered one row later.

spacr.doctor.exit_code(results: Iterable[Result], strict: bool = False) → int[source]

0 when the installation is healthy, 1 otherwise.

Parameters:
  • results – diagnostic rows to inspect. FAIL and ERROR always make the result non-zero; other statuses remain healthy unless strict warning handling applies.

  • strict – also fail on WARN, for CI that wants a clean bill.

spacr.doctor.format_report(results: Sequence[Result]) → str[source]

Render the rows as the text the command prints.

Parameters:

results – rows in the order they should appear — nothing here sorts or groups them, so the printed order is whatever produced the sequence, normally run_checks() in CHECKS order. It must be a real sequence and not a generator: it is traversed three times, once to size the check column to the widest label in this sequence, once for the lines, and once for the trailing counts. fix is printed only for non-PASS rows, so a fix attached to a passing row never reaches the report.

spacr.doctor.main(argv: Sequence[str] | None = None) → int[source]

Run every check, print the report, and return a shell exit code.

Parameters:

argv – arguments without the program name, as argparse.ArgumentParser.parse_args() takes them; None reads sys.argv[1:]. Anything argparse rejects — including --help — exits the process rather than returning, which is the one way a caller embedding this does not get its exit code back. Passing [] runs the full check list against the current working directory with no database and no settings file, which is what plain spacr-doctor does.

spacr.doctor.run_checks(ctx: Context, checks: Sequence[Callable[[Context], Any]] | None = None) → List[Result][source]

Run the selected checks and return their rows.

A check that raises becomes an ERROR row and the run continues: the whole value of this command is the rows it does produce, and losing all of them because one probe hit an unexpected filesystem would be absurd. KeyboardInterrupt is the one exception — the user asking to stop is not a diagnostic finding.

Parameters:
  • ctx – invocation context passed unchanged to every selected check, including the checkout, optional project inputs and GPU-probe choice.

  • checks – the checks to run, in order; None runs every check registered in CHECKS.

spacr.doctor.summarize(results: Iterable[Result]) → Dict[str, int][source]

Count rows by verdict, always returning every key.

Parameters:

results – rows to tally, iterated exactly once, so a generator is safe here (it is not in format_report()). The five standard verdicts are always present in the result even when results is empty, so callers can index them without get; a row carrying some other status string adds a sixth key rather than being dropped.

Nested helpers

_importable_spacr_dirs.add(path: Path) → None

Resolve and append path once, ignoring unresolvable paths.

spacr/doctor.py:457

_register.decorate(function: Callable) → Callable

Label and register function, then return it unchanged.

spacr/doctor.py:146

check_gpu._result(status, message, *, fix='', details=())

Append shared task evidence without changing the GPU diagnosis.

spacr/doctor.py:1293