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. Workers return plain data; GUI-thread completion handlers install it. Changing the source clears the value cache before the replacement schema arrives.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 existing database-file paths |
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.
0schedules the read for a later event-loop turn; edits before that timer fires can still be coalesced.
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]¶
Cancel result delivery and drain both runners when closing mid-read.
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 both runners have attempted their bounded shutdown.
- 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.
In the default threaded mode, returns after dispatching the schema read. Existing column choices and checked values remain visible while it runs, including rules restored before the first source was set. With
threaded=False, the reads run inline.- 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]¶
Cancel pending results and drain both runners with bounded waits.
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 pending results discarded when the user navigates away must call it. Slow database reads can continue in parked workers without delivering their results to this editor. 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 existing database-file paths
sourceresolves 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.