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¶
Add, remove and see the databases a screen will merge. |
Functions¶
|
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.QWidgetAdd, remove and see the databases a screen will merge.
- Parameters:
value – the initial sources. A bare string is accepted because every module’s
srchas 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 whosesrcis 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_COLUMNorNonewhen the “colour by source” box is toggled. Given by a settings panel that owns acolor_byfield; omitted elsewhere, and the box is then not shown.parent – parent widget; ownership only.
threaded – whether the databases are read on a worker thread.
Nonedecides – see_threaded_by_default(). Only a test has any reason to say.title – the caption on the file dialog this opens.
Nonepicks one frommode, 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_finishedcarries 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
Nonewhen they stop.threaded – run the reads on a worker thread;
Nonefollows 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.existsper 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.
- get_value()[source]¶
The chosen sources: a bare string for one, a list for several.
NOT always a list, deliberately.
srchas 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 doesos.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 forcolumn_csv(seesettings_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 touchessrcanyway.
- plan()[source]¶
The last
spacr.multi_database.MergePlan, orNone.
- 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.
- 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._relaycatches theRuntimeErrorPySide6 raises when a worker settles after its runner’s C++ half has gone.
- 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.