spacr.macro

The macro recorder: every run also writes the script that would repeat it.

A GUI run used to leave settings behind and nothing else. The settings are the inputs, not the method — reading settings.json tells you what the knobs were, never which function consumed them, in what order, or how the second module found what the first one made. So the record of an analysis was a folder of numbers plus whatever the analyst remembered.

This module closes that. Every run that opens a journal (spacr.run_journal.open_run() — the one seam the Qt GUI, the Tk GUI and the CLI all launch through) also emits macro.py: real imports, a real settings dict, a real call. Run it and the same thing happens again.

It is three deliverables in one file, and the third is why the emitted script is also a data structure:

A reproducibility record. The script is the method section. Its header carries the spaCR version, the run id and the settings hash, and those are not decoration: the run id is the id spacr.runctx stamped on every log line the run emitted and spacr.artifacts stamped on every output it registered, so the script, the log and the files all join on one column. The settings hash is spacr.artifacts.settings_hash(), the same digest the artifact rows carry.

The on-ramp from clicking to the API. A user who has outgrown the GUI opens macro.py and finds the code they would have written — not a wrapper, not a replay harness, the actual two lines.

Most of the input to the methods-and-results exporter. Which is why the script also carries MACRO, a plain dict literal holding every step’s module, entry point, run id, settings, which of those settings were merely defaults, what it produced and how long it took. read_macro() reads it back without executing the script — it is parsed, not imported — so the exporter can consume a macro it did not generate and does not trust.

Four things the emitted script is careful about

Defaults are written out. A settings dict that omits cell_diameter runs with whatever spaCR’s default is today. Pin the version and that is reproducible; anything less is not. So every key of the module’s defaults is emitted with its value, and MACRO records which keys the user actually set (user_set) and which were filled in (defaulted) — the exporter needs that distinction and the script must not lose it.

A chain is one script. Mask, then Measure, then Classify on the same project is one file with three steps in dependency order, not three files. The edge is confirmed against spacr.ports.next_modules(), so the order in the script is the order the pipeline contract declares.

Intermediate paths are threaded, not repeated. The project each step ran on becomes a named constant, and every path underneath it is rebuilt from that constant with os.path.join(). Repointing the whole chain at another plate is one edit on one line.

Nothing here may fail a run. Recording is bracketed by begin_recording() / finish_recording(), both of which swallow everything. A macro that could not be written costs one log line.

Usage

from spacr.macro import current_macro, read_macro

macro = current_macro()          # the chain recorded in this process
print(macro.source())            # the script
meta = read_macro(macro.path)    # the same thing, as data

Public API

begin_recording, finish_recording

The hook spacr.run_journal.open_run() calls. Everything else is downstream of these two.

Macro, MacroStep, current_macro, macros, reset

The recorded chains, in this process.

render, read_macro, macro_path, macros_dir

Rendering the script, and reading one back as data.

entry_for, module_defaults, explicit_settings

The resolution steps, usable on their own — what function a module key runs, what its defaults are, and the fully-explicit settings dict.

Exceptions

MacroError

A file is not a spaCR macro, or carries a schema this build cannot read.

Classes

Macro

One chain of runs, and the script that repeats it.

MacroStep

One recorded run: what ran, on what, under which id.

Recording

One run being recorded. Created by begin_recording().

Functions

begin_recording(→ Optional[Recording])

Start recording a run. Half of the hook; never raises.

current_macro(→ Optional[Macro])

The chain the next run would join, or None before anything has run.

entry_for(→ Tuple[str, str])

Return (import_path, function_name) for a module key.

explicit_settings(→ Tuple[Dict[str, Any], Tuple[str, ...)

Return the settings a reproduction needs, with nothing left implicit.

finish_recording(→ Optional[MacroStep])

Finish a recording, append its step, write the script. Never raises.

macro_path(→ str)

Return the macro script path inside a run journal folder.

macros(→ Tuple[Macro, ...])

Every chain recorded in this process, oldest first.

macros_dir(→ str)

Return the folder holding one script per recorded chain.

module_defaults(→ Tuple[Dict[str, Any], str])

Return (defaults, source) for a module key.

read_macro(→ Dict[str, Any])

Return the MACRO record of an emitted script.

render(→ str)

Render a Macro as a standalone, runnable Python script.

reset(→ None)

Forget every recorded chain.

summarise(→ str)

Return a one-block human summary of a record from read_macro().

to_json(→ str)

Return a record from read_macro() as JSON.

Module Contents

exception spacr.macro.MacroError[source]

Bases: ValueError

A file is not a spaCR macro, or carries a schema this build cannot read.

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

class spacr.macro.Macro[source]

One chain of runs, and the script that repeats it.

A chain grows while consecutive runs stay connected — the next module consumes what the previous one produced, on the same project. A run that connects to nothing starts a new Macro, because a script that welds two unrelated plates together is not a reproduction of either.

Parameters:
  • macro_id – twelve hex characters, the same shape as a run id.

  • steps – the recorded steps, in the order they ran.

  • created_utc – when the chain started.

  • touched – wall-clock timestamp of the most recent step, used to decide when an idle chain must be closed.

__len__() → int[source]

Return the number of recorded steps in this chain.

__str__() → str[source]

Return the macro id followed by its ordered module chain.

metadata() → Dict[str, Any][source]

Return the machine-readable record the script carries.

source() → str[source]

Render the chain as a runnable Python script.

write(path: Any) → str[source]

Write the script to path and return the path.

Parameters:

path – destination Python-script path.

Written to a neighbouring temporary file and renamed, so a reader that opens it while a later step is being appended never sees half a script.

property modules: Tuple[str, ...][source]

The module keys, in order.

property path: str[source]

The stable script path for this chain, under macros_dir().

class spacr.macro.MacroStep[source]

One recorded run: what ran, on what, under which id.

Parameters:
  • module – the module / app key — "mask", "measure".

  • entry_module – the import path of the entry point.

  • entry_func – the function name.

  • settings – the fully explicit settings dict.

  • defaulted – keys filled in from the module defaults.

  • user_set – keys the caller actually supplied.

  • defaults_source – which source answered; see module_defaults().

  • run_id – the id the run stamped on its log lines and outputs.

  • run_ids – every run id observed during the run, in order. More than one means the pipeline opened nested runs.

  • run_id_source – "runctx" when the id came from the run’s own log records, "journal" when it was taken from the journal folder because no run context opened.

  • settings_hash – spacr.artifacts.settings_hash() over the explicit settings — the digest the artifact rows carry.

  • project_root – the project the step ran on.

  • run_dir – the journal folder.

  • status – "success", "failed", "cancelled".

  • started_utc – when the step opened.

  • finished_utc – when it closed.

  • elapsed_s – how long it took.

  • outputs – declared output locations that exist on disk.

  • link – how this step follows the previous one — "ports" when spacr.ports.next_modules() declares the edge, "project" when they merely share a project, "" for the first step.

  • coerced – settings keys whose value is not a Python literal and was rendered as its string form.

  • spacr_version – the version that ran it.

to_dict(index: int = 0, variable: str = '') → Dict[str, Any][source]

Return the step as the dict MACRO carries.

Parameters:
  • index – the 1-based position in the chain.

  • variable – the name of the settings constant in the script, so a consumer can map a metadata entry back to the code.

property entry: str[source]

"spacr.core.preprocess_generate_masks", or "".

property runnable: bool[source]

Whether this step has an entry point the script can call.

class spacr.macro.Recording[source]

One run being recorded. Created by begin_recording().

Parameters:
  • module – normalized application key used to resolve the recorded entry point, defaults, project root, outputs, and step module.

  • settings – copied launch settings used when finish_recording() receives no override; copying protects them from pipeline mutation.

  • run_dir – run-journal directory copied to the recorded step and used to recover a fallback run id when no log record exposes one.

  • started – time.time() value captured at start and subtracted at finish to produce a nonnegative elapsed duration.

  • started_utc – UTC start timestamp copied into the reproducibility step.

  • capture – optional root-log handler that collects run ids during the invocation; finishing removes and closes it before recording its ids.

spacr.macro.begin_recording(module: str, settings: Mapping[str, Any] | None = None, *, run_dir: Any = '') → Recording | None[source]

Start recording a run. Half of the hook; never raises.

Parameters:
  • module – the module / app key the run was launched for.

  • settings – the settings it was launched with. Copied, because several pipelines mutate the dict they are given and the script must show what was asked for, not what the run left behind.

  • run_dir – the journal folder, when there is one.

Returns:

the Recording to hand finish_recording(), or None when recording could not start — in which case finishing is a no-op and the run is entirely unaffected.

spacr.macro.current_macro() → Macro | None[source]

The chain the next run would join, or None before anything has run.

spacr.macro.entry_for(module: str) → Tuple[str, str][source]

Return (import_path, function_name) for a module key.

Three sources, in the order spacr.qt.bridge.resolve_pipeline_entry() consults them, so the script calls what the Run button called:

  1. spacr.validate.APP_FUNCTIONS, the shipped table;

  2. register_app(..., entry="mod:func"), the registration seam;

  3. a plugin’s entrypoint.

Resolved textually. Rendering a script must not import Cellpose, Torch and pandas to find out what a name is, and a recorder that imported the pipeline it was describing would turn a cheap write into a several-second stall at the end of every run.

Parameters:

module – the module / app key.

Returns:

the pair, or ("", "") when the key names an interactive-only app (Annotate, Make Masks) or nothing at all.

spacr.macro.explicit_settings(module: str, settings: Mapping[str, Any] | None) → Tuple[Dict[str, Any], Tuple[str, ...], str][source]

Return the settings a reproduction needs, with nothing left implicit.

The module’s defaults, then the run-control defaults (spacr.runctx.apply_defaults() — random_seed above all, since an unpinned seed is the one “default” that changes the numbers on its own), then the caller’s values on top. Keys are ordered defaults-first so a diff between two macros lines up.

Parameters:
  • module – the module / app key.

  • settings – what the run was launched with.

Returns:

(settings, defaulted_keys, defaults_source) where defaulted_keys are the keys the caller did not set and the script is therefore pinning on their behalf.

spacr.macro.finish_recording(recording: Recording | None, *, status: str = '', settings: Mapping[str, Any] | None = None) → MacroStep | None[source]

Finish a recording, append its step, write the script. Never raises.

Parameters:
  • recording – what begin_recording() returned. None is a no-op, which is how a recorder that failed to start stays harmless.

  • status – the run’s outcome, as the journal recorded it.

  • settings – override the settings to record.

Returns:

the appended MacroStep, or None when nothing was recorded.

spacr.macro.macro_path(run_dir: Any) → str[source]

Return the macro script path inside a run journal folder.

Parameters:

run_dir – run journal directory that will contain the script.

Deliberately beside manifest.json and settings.json: the journal folder is already what spacr-repro and spacr.notebook_export.export_run() are pointed at, so the script is found by anything that can already find the run.

spacr.macro.macros() → Tuple[Macro, ...][source]

Every chain recorded in this process, oldest first.

spacr.macro.macros_dir() → str[source]

Return the folder holding one script per recorded chain.

~/.spacr/macros, honouring MACRO_DIR_ENV. Created on first use. This is the stable copy: a chain’s file is rewritten in place as each step joins it, so the path does not change while the chain grows. Every run also gets its own copy next to its manifest — see macro_path().

spacr.macro.module_defaults(module: str) → Tuple[Dict[str, Any], str][source]

Return (defaults, source) for a module key.

The sources, in order, are the same ones a settings panel consults — reused rather than re-derived, so the script cannot disagree with the screen it came from:

"registered"

spacr.settings.defaults_for(), the register_defaults seam.

"plugin"

a plugin app’s defaults factory.

"settings_model"

spacr.qt.screens.settings_model.resolve_default_settings(), the built-in dispatch. Guarded: it imports Qt, which a headless run does not have, and a missing Qt must cost the script its default-filling, never the script.

"none"

nothing answered. The settings the caller passed are still emitted in full; only the unset keys are missing, and MACRO says so through defaults_source.

Parameters:

module – the module / app key.

Returns:

a fresh dict, and the name of the source that produced it.

spacr.macro.read_macro(path: Any) → Dict[str, Any][source]

Return the MACRO record of an emitted script.

Parsed, never executed. The exporter’s input is a file spaCR may not have written — someone else’s macro, an edited one, one from a newer build — and importing it would run whatever is in it. So this walks the AST, evaluates the top-level literal assignments (which is how the PROJECT_1 and *_SETTINGS names the metadata refers to are resolved) and returns the record.

Parameters:

path – the script.

Returns:

the record, with each step’s "settings" resolved to the real dict.

Raises:
  • MacroError – when the file has no MACRO, or its schema is newer than this build understands.

  • OSError – when the file cannot be read.

  • SyntaxError – when it is not Python at all.

spacr.macro.render(macro: Macro) → str[source]

Render a Macro as a standalone, runnable Python script.

Parameters:

macro – the chain to render.

Returns:

the source. Always parses: tests/test_macro.py compiles every script this function produces, because a recorder that emits plausible-looking code that does not run is worse than no recorder.

spacr.macro.reset() → None[source]

Forget every recorded chain.

The next run starts a fresh one. For a test, and for a caller that wants a chain boundary it chose rather than one inferred from the project layout.

spacr.macro.summarise(record: Mapping[str, Any]) → str[source]

Return a one-block human summary of a record from read_macro().

Parameters:

record – decoded macro metadata record to summarize.

What a methods section starts from: the version, the chain, and per step the entry point, the run id and how many settings were the user’s rather than defaults.

spacr.macro.to_json(record: Mapping[str, Any], **kwargs: Any) → str[source]

Return a record from read_macro() as JSON.

Parameters:

record – decoded macro metadata record to serialize.

For a consumer that would rather have JSON than a Python dict — the methods exporter prompt, a web view, a diff. default=str so a value that survived as a coerced string does not take the dump down.