spacr.qt.widgets.row_exclusion¶
Column/value editor for excluding rows from UMAP input data.
Everything this widget offers the user comes out of a measurements database, and both reads used to happen on the GUI thread:
discover_columnsopens every database thesrcsetting resolves to and runssqlite_masterplus aPRAGMA table_infoper table. It is driven fromSettingsWidgets._refresh_contextual_widgets, so it runs every timesrcortablesis set.distinct_valuesruns oneSELECT DISTINCT … LIMIT 501per(database, table)pair the chosen column appears in — and the columns a user actually excludes on (plateID,columnID,rowID) hold a handful of distinct values across the whole table, so the LIMIT is never reached and SQLite scans every row. Measured on a 200 000-row × 8-table measurements.db with a warm page cache: 196 ms per column, and the column combo is editable, socurrentTextChangedfires on every keystroke. Eight quick edits froze the window for 894 ms in one unbroken block;set_sourceitself cost 183 ms and a single deliberate column choice 220 ms. Threaded, the same three are 29 ms, 5 ms and 2 ms.
So both go through a JobRunner owned by the
editor, and the value reads are additionally debounced. Three rules hold
this together:
_value_cacheand_column_sourcesare GUI-thread state. The worker returns plain data and the completion handler — which Qt runs on the GUI thread — is the only thing that writes them.Schema discovery and value reads get separate runners.
JobRunner.cancel()works by generation, so one runner cannot supersede a value read without also dropping the schema read that the value read depends on.A debounce timer coalesces keystrokes, and
JobRunner.cancel()drops whatever an earlier keystroke started, so the last edit wins rather than the slowest read.
Classes¶
Add one or more UMAP row exclusions by choosing columns and values. |
Functions¶
|
Map every column name to the |
|
Return the distinct values of |
|
Return the |
Module Contents¶
- class spacr.qt.widgets.row_exclusion.RowExclusionEditor(value=None, parent=None, *, threaded: bool = True, debounce_ms: int = DEBOUNCE_MS)[source]¶
Bases:
PySide6.QtWidgets.QWidgetAdd one or more UMAP row exclusions by choosing columns and values.
The database reads behind the two dropdowns run on worker threads; see the module docstring for the measured reason. Nothing about the widget’s public contract changed —
set_source()still takes a source and returns, it just no longer waits for sqlite before it does.- Parameters:
value – initial rules, in any form
spacr.row_exclusions.normalize_row_exclusions()accepts.parent – Qt parent.
threaded –
Falseruns both reads inline, in the same order, emitting the same signals — a caller that must have the values the instantset_source()returns can ask for it. Keyword only, and defaulted, because the shipped call site (SettingsWidgets._widget_for) passesvalueandparentand nothing else.debounce_ms – how long a column edit waits before it is read.
0reads on the next event-loop turn without coalescing.
Build the row-exclusion editor.
Two runners, not one:
cancelabandons everything a runner has in flight, and superseding a keystroke’s value read must not also abandon the schema read that says which databases the column even lives in.- Parameters:
value – the exclusions to start with.
parent – parent widget, or
None.threaded – read on worker threads. Set
Falsein tests so a read finishes before it returns.debounce_ms – how long typing settles before a value read is run.
- closeEvent(event)[source]¶
Closing mid-read must not leave a thread behind.
Qt aborts the process when a running QThread is destroyed, and a worker that delivers into a widget on its way out is a use-after-free.
JobRunner.shutdownhandles both.Not the only line of defence, deliberately, because it is not always reached: this editor is a child inside a settings panel, and navigating away from that panel destroys it without any close event. What covers that case is the runner itself — the QThreads are unparented and retire themselves, and
JobRunner._relaycatches theRuntimeErrorPySide6 raises when a worker settles after its runner’s C++ half has gone.- Parameters:
event – the close event; passed on unchanged to the base class after the workers are shut down.
- get_value() dict[str, list[Any]] | None[source]¶
The exclusions, in the shape the settings dict wants.
- Returns:
{column: [values]}, or None when nothing is excluded.
- set_source(source, tables=None) None[source]¶
Populate column/value choices from a dropped measurements DB.
Returns as soon as the schema read is dispatched. The dropdowns keep whatever they are showing until the worker delivers — a list that is half a second stale beats a frozen window, and on the first call they are showing nothing anyway.
- Parameters:
source – anything
source_paths()accepts: a run folder, itsmeasurementsfolder, a.dbfile, or a list of them.tables – restrict the columns to these table names;
Nonereads every table.
- set_value(value) None[source]¶
Replace the exclusions from a settings value.
- Parameters:
value –
{column: [values]}, or None to clear.
- shutdown() None[source]¶
Stop reading and let no worker outlive the widget.
Public because a host that owns this editor inside a larger screen can call it directly — Qt delivers a close event to the window, not to every widget inside it, so a screen that wants the reads stopped the moment the user navigates away has to say so. Idempotent.
- spacr.qt.widgets.row_exclusion.discover_columns(source, tables=None) dict[str, list[tuple[pathlib.Path, str]]][source]¶
Map every column name to the
(database, table)pairs holding it.Pure and Qt-free so it can run on a worker thread; the result is plain data the GUI thread installs.
- Parameters:
source – anything
source_paths()accepts.tables – restrict to these table names; falsy means all of them.
- spacr.qt.widgets.row_exclusion.distinct_values(sources: Iterable[tuple[pathlib.Path, str]], column: str, limit: int = VALUE_LIMIT) list[Any][source]¶
Return the distinct values of
columnacrosssources, sorted.The slow half of this widget, and the reason it has a worker thread. Pure and Qt-free — hand it the
(database, table)pairsdiscover_columns()found and it touches nothing else.Every connection is closed in a
finally. The version this replaces usedwith sqlite3.connect(...) as connection, which commits but does not close, so a session of typing in the column box leaked one file descriptor onto a multi-hundred-megabyte database per keystroke.- Parameters:
sources –
(database path, table name)pairs to read; a database or table that cannot be read is skipped.column – the column name; empty returns
[].NULLvalues are left out.limit – stop after this many distinct values.
- spacr.qt.widgets.row_exclusion.source_paths(source) list[pathlib.Path][source]¶
Return the
measurements.dbfilessourceresolves to.Accepts a path, a list of paths, or the repr of a list (which is what a settings text field holds after a multi-folder drop), and tolerates a run folder, its
measurementssubfolder, or the database itself. Paths that do not exist are dropped rather than reported: this feeds a dropdown, and a half-typedsrcis not an error.- Parameters:
source – a path, a list or tuple of paths, or the text of such a list; empty entries are skipped and
~is expanded.