spacr.qt.widgets.database_set

The set of measurement databases a screen is working on.

Where a workflow accepts one database, this widget lets you add, remove, and inspect several. It uses spacr.qt.widgets.table_chip.TableChip, already used for the Gate Editor’s table working set. Databases get the same control rather than a second one for the same idea.

WHAT THIS WIDGET IS FOR, beyond holding a list of paths:

THE ANSWER HAS TO ARRIVE BEFORE THE USER COMMITS.

The column set produced by a merge defines the analysis about to run, and finding out afterwards that half the measurements were dropped is finding out too late. So every time the set changes this asks spacr.multi_database.describe_merge() – which reads only sqlite metadata and the distinct plate ids – and puts the answer on screen: how many rows, how many columns are common, which measurements are in only some, and whether two databases claim the same plate.

AND IT ASKS OFF THE GUI THREAD. That read is cheap in the VOLUME it reads and it is not cheap in LATENCY, and this file used to hold the first as a reason to believe the second: it stats every database and then opens each one with sqlite five times over, on paths that are folders the user chose. Measured on one workstation, a single os.path.exists under an autofs mount whose share was asleep had not returned after TWENTY SECONDS. This widget is built while a settings panel is laid out and refreshed on every drop, so that was the whole application frozen, and it left no traceback because a stalled event loop is not a crash. Everything the summary needs is now read by _read_the_merge() on a worker thread (spacr.qt.job_runner) and painted when it lands: every line this widget used to print, it still prints, a moment later.

A COLLISION IS SHOWN, NOT RESOLVED. Two databases that each hold a plate1 are two experiments, and the widget says so and names them. It deliberately does NOT offer on_collision='qualify': rewriting plate1 to runA-plate1 makes the keys unique and hides which experiment a plate came from inside its own id, where nothing can block on it, test for it or colour by it. The resolutions offered are the ones that keep the experiment analysable – remove one of the databases, or rename the plates.

Classes

DatabaseSetWidget

Add, remove and see the databases a screen will merge.

Functions

database_for_source(→ str)

The database a chosen source names.

Module Contents

class spacr.qt.widgets.database_set.DatabaseSetWidget(value=None, parent=None, *, mode: str = 'database', table: str = 'cell', title: str | None = None, min_items: int = 0, on_colour_by: Callable[[str | None], None] | None = None, threaded: bool | None = None)[source]

Bases: PySide6.QtWidgets.QWidget

Add, remove and see the databases a screen will merge.

Parameters:
  • value – the initial sources. A bare string is accepted because every module’s src has historically been one, and a settings CSV written before this widget existed still holds one.

  • mode'database' (the user picks .db files – the Gate Editor) or 'folder' (the user picks plate folders – Image UMAP and every other module whose src is a project root).

  • table – the table the merge is described on. 'cell' is the anchor every object join is built on.

  • min_items – how many sources may never be removed. The Gate Editor passes 1, because a gate editor with no table is a screen with nothing on it; a settings panel passes 0, because clearing the field is a legitimate thing to want.

  • on_colour_by – called with spacr.multi_database.SOURCE_COLUMN or None when the “colour by source” box is toggled. Given by a settings panel that owns a color_by field; omitted elsewhere, and the box is then not shown.

  • parent – parent widget; ownership only.

  • threaded – whether the databases are read on a worker thread. None decides – see _threaded_by_default(). Only a test has any reason to say.

  • title – the caption on the file dialog this opens. None picks one from mode, so it only needs giving when the dialog is asking for something more specific than “a database” or “a folder” – the dialog’s title is the only prompt it has.

Build the multi-database chooser.

The reads leave the GUI thread deliberately: the summary stats every database and then opens each with sqlite, over paths that are the user’s own – one of them an autofs mount that had not answered a single stat after twenty seconds – and doing that while a settings panel lays out froze the application. Two runners rather than one, because job_finished carries a bool and no job identity: a shared runner could not tell whose failure it was being told about, so the restore check failing would clear the summary read’s in-flight flag and paint its error over a read still running.

Parameters:
  • value – the sources to start with.

  • parent – parent widget, or None.

  • mode"folder" for spaCR project folders, anything else for measurement databases.

  • table – which table the merge summary is computed over.

  • title – the file-dialog caption; defaults by mode.

  • min_items – how many sources must remain; chips below this are not removable.

  • on_colour_by – called with the source column when the user asks for the map to be coloured by database, and with None when they stop.

  • threaded – run the reads on a worker thread; None follows the process default.

add_sources(paths: Sequence[str]) int[source]

Add sources, ignoring the ones already in the set.

Returns:

how many were actually added.

apply_workspace_state(state) bool[source]

Re-attach the set. Returns whether anything was attached.

A source that is no longer there is left out and the rest are still attached – one moved plate must not cost the user the other three. The workspace document names every missing one in its own report, so nothing dropped here is dropped silently.

WHICH ONES ARE STILL THERE IS ASKED OFF THE GUI THREAD. This was an os.path.exists per source, inline, and these are the paths of a workspace saved days ago – precisely the ones most likely to sit on a mount that has since gone to sleep, and reopening a saved session is not a moment to freeze the application in. So the whole set is attached now and the ones that turn out to be gone are dropped when the answer lands. Attaching a moved plate and removing it a moment later costs one redraw; waiting twenty seconds for its stat costs the session.

WHAT THE RETURN VALUE MEANS NOW, because it is the one thing the move changed: it answers “was there anything to attach”, not “was any of it still on disk”. spacr.workspace.restore_workspace() prints the section as restored either way, where a set whose every source had moved used to be printed as declined. Nothing is hidden from the user by that – the ones that turn out to be gone are dropped when the answer lands, and the summary line names every source with no database behind it – but the report line is a moment earlier than the truth, and the alternative is blocking the restore on the stat, which is the freeze. Unthreaded (a test, and every other test file here) the check has already run by the time this returns, so the old answer stands.

choose_sources() None[source]

Open the picker and ADD what comes back.

The dialog is opened at "" – no remembered root. That is the one reason this stays on the GUI thread while the rest of the file’s filesystem work moved off it: there is no path of the user’s to stat before the dialog exists, and listing the directory it lands in is the thing the user asked for, in a window that is its own progress indicator. A remembered start directory would put a possibly-sleeping mount back in front of the event loop; do not add one here.

clear() None[source]

Empty the set.

closeEvent(event)[source]

Closing mid-read must not leave a worker behind.

database_paths() List[str][source]

The database each source names – what a merge would open.

get_value()[source]

The chosen sources: a bare string for one, a list for several.

NOT always a list, deliberately. src has been a string for every module since spaCR had modules, and it is written to the settings CSV, read by the CLI, replayed by the run journal and joined onto by anything that does os.path.join(src, ...). Returning ['/data/plate1'] where a string was returned before would change what every one of those sees for a user who chose ONE folder and wanted nothing to do with this feature – exactly the regression the one-element list caused for column_csv (see settings_model.PATH_LIST_SINGLE_KEYS).

Several sources produce a list, which is the shape spacr.core.generate_image_umap() builds on the first line it touches src anyway.

plan()[source]

The last spacr.multi_database.MergePlan, or None.

remove_source(name: str) bool[source]

Drop one member of the set, by its chip’s label or its path.

The chip carries the LABEL – plate1, not /data/plate1/measurements/measurements.db – because that is what a legend and the provenance column say. Both are accepted so a caller with the path does not have to work out the label first.

set_value(value) None[source]

Replace the set. Accepts a list, a bare string, or None.

shutdown() None[source]

Stop reading; let no worker outlive the widget. Idempotent.

Qt ABORTS THE PROCESS when a running QThread is destroyed, and this widget now starts them. Public because it is a CHILD – a settings panel builds it inside a form, and Qt delivers a close event to the window rather than to every widget in it, so a host that wants the reads stopped when the user navigates away has to say so.

Not the only line of defence: the QThreads are unparented and retire themselves, and JobRunner._relay catches the RuntimeError PySide6 raises when a worker settles after its runner’s C++ half has gone.

sources() List[str][source]

The chosen sources.

workspace_state() dict[source]

The attached set, IN ORDER, and the databases behind it.

Order is state, not presentation: a merge resolves a column collision in favour of the first source that has it, so the same two databases in the other order are a different merged table.

Both the sources and the databases they resolve to are written down. A source is a folder the user chose; the database is the file the merge opens, and it is the one whose absence or edit a restore has to be able to report.

spacr.qt.widgets.database_set.database_for_source(source: str, mode: str = 'database') str[source]

The database a chosen source names.

Parameters:
  • source – what the user picked – a database file in 'database' mode, a spaCR project/plate folder in 'folder' mode.

  • mode'database' or 'folder'.

Returns:

the path to open. Nothing is checked for existence; a source whose database is missing is reported by the summary rather than removed from the set behind the user’s back.