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.

Besides holding a list of paths, the widget does the following:

It describes the merge before the user runs it.

The column set produced by a merge defines the analysis about to run, 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.

The description is read off the GUI thread. The read is small but can be slow: it stats every database and then opens each one with sqlite five times over, on folders the user chose, and a single os.path.exists under an autofs mount whose share was asleep has been measured not returning after twenty seconds. Everything the summary needs is read by _read_the_merge() on a worker thread (spacr.qt.job_runner) and painted when it arrives.

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 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.

Parameters:

paths – source folders or database paths to append; each is converted with str() and stripped, and blanks and duplicates are skipped. None adds nothing.

Returns:

how many were actually added.

apply_workspace_state(state) → bool[source]

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

Saved sources retain their order. In folder mode, an explicit matching databases list can supply a database restored by the workspace core. If the original database is missing and that file exists, the set reads the supplied file while retaining the original source key. This association does not rewrite the source roots returned by get_value() or reconstruct a project folder for a new pipeline.

Filesystem checks run off the GUI thread. Sources with neither an existing original source nor a usable explicit database are dropped when the answer arrives. Added sources are preserved; replaced, removed and re-added sources cannot receive stale associations.

The return value answers “was there anything to attach”, not “was any of it still on disk” when the check is asynchronous. The workspace core reports missing or changed files separately. Unthreaded, the check has already run by the time this returns.

Parameters:

state – dict saved by workspace_state(); reads sources, mode and an optional positionally matching databases list. A non-dict or empty source set returns False. File verification and carried-path remapping belong to the workspace core.

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]

Stop any reading worker before the widget closes.

Parameters:

event – the close event; it is not inspected, only passed on to the base class after shutdown().

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. 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 – 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.

Parameters:

name – the source’s path or its chip label; nothing is removed when it matches neither or the set is already at its minimum size.

set_value(value) → None[source]

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

Parameters:

value – a list of source paths, a single path string, or None; paths are stripped and de-duplicated, and settings placeholders such as path are dropped.

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 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.