spacr.cli

spacr-run — run any spaCR module from a settings file, with no GUI.

Every spaCR pipeline has, until now, been reachable only through a GUI: the PySide6 app (spacr / spacr-qt) or the classic Tk app (spacr-tk). That is fine on a workstation and impossible on a cluster — importing either entry point pulls Qt or Tk, and a compute node has no display to give them.

This module is the headless path. It is deliberately, testably light: importing spacr.cli must not pull Qt, Tk, torch, cellpose, numpy or pandas. Everything heavy is imported inside the command that needs it, so spacr-run --help and spacr-run --list answer instantly on a login node, and --dry-run validates a 40-plate settings file without touching a GPU.

Usage:

spacr-run <module> --settings settings.csv [--set key=value ...] [--dry-run]
spacr-run --list                  # every module that can run headless
spacr-run --describe measure      # what it does, what it needs, what it writes
spacr-run validate --settings f --module mask     # pre-flight only

The settings file is the one the GUI writes. Both spaCR CSV layouts are read — Key,Value (what spacr.utils.save_settings() emits next to every run) and setting_key,setting_value (the documented default of spacr.utils.load_settings()) — plus the settings.json written into each run-journal folder. So the round trip is: click through the GUI once on a laptop, copy <src>/settings/gen_mask_settings.csv to the cluster, and sbatch it unchanged.

Exit codes (a cluster job that exits 0 after failing is the classic headless footgun, so spaCR’s own codes are exact):

0 the module ran to completion, or the dry run / validation found no errors 1 the module raised 2 bad arguments, unreadable settings, or pre-flight found errors

A pipeline that raises SystemExit itself is the exception: its integer code is passed through unchanged, so any value can reach the shell.

Matplotlib is forced to Agg when there is no display, and plt.show is replaced by a close-the-figure shim for the duration of the run — the same thing spacr.gui_utils.spacrFigShow() does inside the GUI — so a pipeline that calls plt.show() neither blocks nor leaks figures.

Exceptions

SettingsError

A settings file, an override or a module name the user got wrong.

Classes

Module

One headless-runnable spaCR pipeline.

Functions

apply_overrides(→ Dict[str, Any])

Apply --set key=value overrides on top of a settings dict.

build_parser(→ argparse.ArgumentParser)

Return the spacr-run argument parser.

cmd_describe(→ int)

--describe <module> — print one module's contract.

cmd_list(→ int)

--list — print every module that can run headless.

cmd_run(→ int)

<module> --settings f — the real thing, or --dry-run for the plan.

cmd_validate(→ int)

validate --settings f — pre-flight only, nothing is executed.

coerce_value(→ Any)

Coerce a --set key=value string into the type the setting expects.

import_entry(→ Callable[..., Any])

Import and return the pipeline callable for module.

load_settings_file(→ Dict[str, Any])

Load a settings file written by the GUI, a pipeline run or a run journal.

main(→ int)

spacr-run entry point.

module_defaults(→ Dict[str, Any])

Return a fresh defaults dict for module.

render_module_description(→ str)

Render the --describe block for one module.

render_module_list(→ str)

Render the --list table of headless-runnable modules.

render_settings(→ str)

Render a resolved settings dict as an aligned, sorted table.

resolve_module(→ Optional[Module])

Return the Module for a user-typed name, or None.

resolve_settings(, moved, Any])

Build the settings dict the pipeline will actually receive.

setup_logging(→ logging.Logger)

Configure timestamped logging to stdout for a batch run.

use_agg_if_headless(→ bool)

Force matplotlib's Agg backend when there is no display.

Module Contents

exception spacr.cli.SettingsError[source]

Bases: Exception

A settings file, an override or a module name the user got wrong.

Always maps to exit code 2: the run never started, so it is an argument problem rather than a runtime failure.

Initialize self. See help(type(self)) for accurate signature.

class spacr.cli.Module[source]

One headless-runnable spaCR pipeline.

Parameters:
  • key – name the user types, matching the GUI’s app_key.

  • summary – one line for --list.

  • entry – "module:function" of the callable that does the work.

  • defaults – name of the spacr.settings helper that fills the defaults, or None when the pipeline has none there.

  • validate_key – app key understood by spacr.validate; empty when that module has no specific rules and only generic checks apply.

  • requires – settings that must be supplied, phrased for a human.

  • writes – what lands on disk.

  • call_style – "settings" for fn(settings_dict); "folder" for fn(settings["src"]). No built-in module uses "folder"; it reaches spaCR only through a plugin app that declares it.

  • note – caveat worth printing in --describe.

  • defaults_entry – "module:function" of a defaults helper that does not live in spacr.settings. Six built-in pipelines keep their own (foreign, external_masks, convert, illumination, barcode_qc, anndata_export), as does every plugin app, because their keys are theirs alone; without this the CLI would resolve an empty defaults dict for them, and --set would then reject every one of their keys as a setting that does not exist.

property defaults_label: str[source]

How --describe names this module’s defaults helper.

property func_name: str[source]

Attribute name of the callable inside module_name.

property module_name: str[source]

Import path of the module holding entry.

spacr.cli.apply_overrides(settings: Dict[str, Any], overrides: Sequence[str], module: Module | None = None) → Dict[str, Any][source]

Apply --set key=value overrides on top of a settings dict.

An override naming a key spaCR does not know is an error, not a no-op: a typo’d override that quietly does nothing costs a whole run to discover, and the run looks like it succeeded.

So is an override naming a key spaCR does know but nothing reads. Those are worse, because they pass every “is this a real setting?” test there is: remove_border_pathogens is typed, tooltipped and offered by the Pathogen category, and spacr-run mask --set remove_border_pathogens=True was accepted in silence and did nothing. spacr.settings.DEAD_SETTINGS names every such key and the spelling that works instead.

Parameters:
  • settings – settings resolved from defaults plus file; mutated in place.

  • overrides – raw key=value strings from the command line.

  • module – the module being run, used only for the error message.

Returns:

settings.

Raises:

SettingsError – on an unknown key, a key nothing reads, or an uncoercible value.

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

Return the spacr-run argument parser.

Building the parser imports nothing beyond the standard library, so spacr-run --help is instant even on a node with a cold NFS cache.

Returns:

the parser.

spacr.cli.cmd_describe(name: str) → int[source]

--describe <module> — print one module’s contract.

spacr.cli.cmd_list(_args: argparse.Namespace) → int[source]

--list — print every module that can run headless.

spacr.cli.cmd_run(args: argparse.Namespace) → int[source]

<module> --settings f — the real thing, or --dry-run for the plan.

spacr.cli.cmd_validate(args: argparse.Namespace) → int[source]

validate --settings f — pre-flight only, nothing is executed.

spacr.cli.coerce_value(key: str, text: str, current: Any, expected_types: Mapping[str, Any], app: str = '') → Any[source]

Coerce a --set key=value string into the type the setting expects.

The type comes from spacr.settings.expected_types when the key is declared there, otherwise from the type of the value the key already holds. A value that cannot be coerced is an error rather than a silently-stored string: cell_mask_dim='4' is exactly the bug the settings CSV round trip keeps producing, and measure_crop only notices it an hour in.

Parameters:
  • key – settings key.

  • text – the raw text after the first =.

  • current – the value key holds before the override.

  • expected_types – spacr.settings.expected_types.

  • app – module key, so a key two pipelines share is read as the module being run means it (see _APP_TYPE_OVERRIDES).

Returns:

the coerced value.

Raises:

SettingsError – when text is not a legal value for key.

spacr.cli.import_entry(module: Module) → Callable[..., Any][source]

Import and return the pipeline callable for module.

Deliberately late: this is where torch, cellpose and the rest of the heavy stack finally load, long after --help and --list have answered.

Parameters:

module – module whose entry point is wanted.

Returns:

the callable.

Raises:

SettingsError – when the module or attribute cannot be imported.

spacr.cli.load_settings_file(path: Any) → Dict[str, Any][source]

Load a settings file written by the GUI, a pipeline run or a run journal.

Parameters:

path – path to a .csv or .json settings file.

Returns:

the settings dict.

Raises:

SettingsError – when the path is missing, unreadable or malformed. Never a traceback — a cluster job should fail with a sentence.

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

spacr-run entry point.

Parameters:

argv – argument list; sys.argv[1:] when None.

Returns:

process exit code — 0 success, 1 the module raised, 2 bad arguments or settings. Any other integer comes from a pipeline that raised SystemExit itself; cmd_run() passes that code through unchanged.

spacr.cli.module_defaults(module: Module) → Dict[str, Any][source]

Return a fresh defaults dict for module.

Calls the same helper the pipeline itself uses to canonicalize its settings, so the resolved dict the CLI prints is the one the pipeline will see. That is usually a spacr.settings function (Module.defaults); for the pipelines that keep their own it is Module.defaults_entry, imported here rather than at module load so --list stays instant.

Parameters:

module – the module whose defaults are wanted.

Returns:

dict of defaults; empty when the pipeline has no helper.

Raises:

SettingsError – when the defaults module will not import. This used to return {} so that --describe survived a missing optional dependency, but resolve_settings() is the run path, not just the describe path: convert, illumination, foreign, external_masks, barcode_qc, anndata_export and every plugin app then ran on a settings dict with no defaults in it, and spacr-run convert --set z_handling=max was rejected with “names a setting that does not exist for module ‘convert’” — pointing the user at their own command line instead of at the dependency that is actually missing. --describe is unaffected: it has its own guard around this call.

spacr.cli.render_module_description(module: Module) → str[source]

Render the --describe block for one module.

Parameters:

module – module to describe.

Returns:

the description as one string, no trailing newline.

spacr.cli.render_module_list() → str[source]

Render the --list table of headless-runnable modules.

Returns:

the table as one string, no trailing newline.

spacr.cli.render_settings(settings: Mapping[str, Any]) → str[source]

Render a resolved settings dict as an aligned, sorted table.

Parameters:

settings – the resolved settings.

Returns:

the table as one string, no trailing newline.

spacr.cli.resolve_module(name: Any) → Module | None[source]

Return the Module for a user-typed name, or None.

Parameters:

name – module key, alias, or the bare name of the pipeline function.

Returns:

the matching Module, or None when nothing matches.

spacr.cli.resolve_settings(module: Module, settings_path: str | None, overrides: Sequence[str] = (), moved: List[Any] | None = None) → Dict[str, Any][source]

Build the settings dict the pipeline will actually receive.

Layered lowest-to-highest: the module’s own defaults, the settings file, then the --set overrides. The file is read under today’s names first – see _under_todays_names(), without which a value the file set under an old name lost to the default already sitting under the new one.

Parameters:
  • module – module being run.

  • settings_path – path to the settings file, or None for defaults only.

  • overrides – key=value strings.

  • moved – list to receive the migration notices, passed straight to _under_todays_names(); a caller that prints a pre-flight report hands these to _preflight() so the keys the fold consumed are still named there.

Returns:

the fully-resolved settings dict.

Raises:

SettingsError – on any unreadable file, unknown key or bad value.

spacr.cli.setup_logging(verbose: bool = False) → logging.Logger[source]

Configure timestamped logging to stdout for a batch run.

Parameters:

verbose – raise the level from INFO to DEBUG.

Returns:

the spacr.cli logger.

spacr.cli.use_agg_if_headless() → bool[source]

Force matplotlib’s Agg backend when there is no display.

Called before the first spaCR import that could pull pyplot. An explicit MPLBACKEND in the environment always wins, and interactive local use (a display is present) is left alone, so this only bites on a compute node.

Returns:

True when Agg was forced.

Nested helpers

_NoShow.__enter__._close_instead(*args: Any, **kwargs: Any) → None

Close the captured current figure instead of displaying it.

Parameters:
  • args – ignored positional arguments accepted for show signature compatibility.

  • kwargs – ignored keyword arguments accepted for compatibility.

Returns:

None. Backend and close errors are deliberately swallowed so optional visualization cleanup cannot fail a headless run.

spacr/cli.py:1387