spacr.qt.widgets.file_list¶
Edit ordered settings that accept one or more file paths.
FilePathListWidget appends files from a multi-select dialog or by
dropping files and folders. A dropped folder contributes sorted matching files
from one directory level. Duplicate paths are rejected, missing paths are
marked before execution, and entries can be reordered when plate order matters.
The widget reads and writes a plain list[str] so Qt settings, settings CSVs,
and command-line calls share the same value representation.
Classes¶
An ordered, de-duplicated list of input paths with picker and drop. |
|
Editable one-row-per-plate score/count input and measurement link. |
Functions¶
|
|
|
|
|
Propose visible score/count/database rows by filename tokens. |
Module Contents¶
- class spacr.qt.widgets.file_list.FilePathListWidget(value: Any = None, *, kind: str = 'table', title: str = 'Choose input files', allow_folders: bool = True, single: bool = False, parent=None)[source]¶
Bases:
PySide6.QtWidgets.QWidgetAn ordered, de-duplicated list of input paths with picker and drop.
single=Trueis the same control for a setting that names exactly ONE file. It keeps the file dialog and the drop target – which is the whole reason these settings stopped being text boxes – but the value it holds and returns is a plainstr, choosing again REPLACES rather than appends, and the reorder buttons are gone because one path has no order.That distinction is not cosmetic.
grna_csv,row_csvandcolumn_csvare declaredstrand go straight topd.read_csv, so rendering them as a list turned a working default into['/path/to/barcodes_row.csv']the moment the screen was opened and saved. Every run from that file was then refused by the pre-flight – “column_csv=[…] is a list, but str is expected” – against a value the user had never typed and could not correct from the panel that wrote it.- Parameters:
value – what the setting already holds. A bare string stays a string – see above; that is the whole point of this widget.
kind – which file filter the chooser opens with, one of
FILE_KIND_FILTERS. An unrecognised name falls back to"any"rather than raising, so a new setting cannot break a panel by naming a filter that does not exist yet.title – the file dialog’s window title.
allow_folders – whether a folder may be picked. Forced off when
singleis set: expanding a folder into “every CSV in here” cannot mean anything for a setting that names ONE file.single – whether the setting holds one path rather than a list.
parent – parent widget.
Build the ordered, de-duplicated path list.
- Parameters:
value – the paths already saved.
kind – what sort of file it accepts.
title – the caption on its picker.
allow_folders – whether folders may be added.
single – whether only one path is allowed.
parent – parent widget.
- add_paths(paths: Iterable[Any]) int[source]¶
Append
paths, expanding folders. Returns how many were added.When the setting names ONE file this REPLACES what is there. A second choice is a correction, and a control that appended left the run reading a file the user believed they had swapped out.
- Parameters:
paths – paths to add, as strings or path-like objects; a bare string counts as one path, and placeholders and None entries are skipped. Each is made absolute, a folder contributes its matching files one level down, and a single-file widget keeps only the last.
- dragEnterEvent(event: PySide6.QtGui.QDragEnterEvent) None[source]¶
Accept a drag carrying file URLs.
- Parameters:
event – the Qt drag event.
- dragMoveEvent(event: PySide6.QtGui.QDragMoveEvent) None[source]¶
Keep accepting while file URLs stay over the list.
- Parameters:
event – the Qt drag event.
- dropEvent(event: PySide6.QtGui.QDropEvent) None[source]¶
Add the dropped paths, ignoring a drop that carries none.
- Parameters:
event – the Qt drop event.
- get_value() Any[source]¶
The setting’s value: a
list[str], or astrwhen single.The shape is the SETTING’s, not the widget’s. A key declared
strthat came back as a one-element list rewrote the user’s settings file on open and reachedpd.read_csvas a list.
- pick_files() int[source]¶
Open the file dialog and take what is chosen.
Multi-select, except when the setting names one file – a dialog that lets you pick four when three of them will be discarded is a control lying about what it does.
- pick_folder() int[source]¶
Ask for a folder and add it.
- Returns:
how many paths were added; 0 when cancelled or duplicate.
- remove_selected() None[source]¶
Drop the selected paths.
Removed from the BOTTOM up, so each row index is still valid when it is reached – deleting top-down shifts everything below it.
- set_value(value: Any) None[source]¶
Replace the contents. Accepts None, a str, or any iterable.
A single-file widget keeps only the last of whatever it is given. That is what loads a settings file written while these keys were wrongly rendered as lists:
['/x/barcodes_row.csv']comes back as/x/barcodes_row.csvrather than carrying the wrong shape forward.- Parameters:
value – None, one path as a string or path-like object, or an iterable of paths; surrounding quotes and placeholder values are dropped.
- class spacr.qt.widgets.file_list.PairedFileTableWidget(value=None, parent=None)[source]¶
Bases:
PySide6.QtWidgets.QWidgetEditable one-row-per-plate score/count input and measurement link.
A row is ONE PLATE: its score and count source paths, optional named tables within those sources, and the separate measurements database that plate’s per-object tables live in. The source paths are filled BY ADDITION – every arrival re-proposes the whole table from filename tokens – so databases dropped in the opposite order to the files still land on the right plates. That is the rule, and the third column obeys it rather than keeping a list of its own.
A plate with NO database is legal and is not an error: the regression is fitted on scores and counts. The database is what makes that plate’s measurements available downstream, so its absence disables the plate there instead of failing the run.
- Parameters:
value – the table already saved, one entry per plate. Everything that arrives afterwards RE-PROPOSES the whole table from filename tokens rather than appending, which is what lets databases dropped in the opposite order to the source files still land on the right plates.
parent – parent widget.
Build the one-row-per-plate table.
- Parameters:
value – the rows already saved.
parent – parent widget.
- add_paths_for_side(paths, side: str = 'score') int[source]¶
Add files to one column of the table and re-propose the pairing.
The drop router calls this method after identifying a file’s role from its header, so a count table dropped anywhere on the widget still fills the count column.
'database'is the third side. It goes through this same method, and therefore through the same whole-table re-proposal, rather than through an adder of its own that would keep a private list and pair by the order things arrived.- Parameters:
paths – File paths to add.
side – Target column:
'score','count', or'database'.
- Returns:
Number of previously absent files added.
- align_download_buttons() bool[source]¶
Put the Download buttons over the columns they fill.
- Returns:
True when the strip was found and is now following this table’s header; False when this table was not built into a screen that has a Download row – which is every use of it outside Regression’s Input Tables, and every test that builds it bare.
- attach_database(path, row=None) str[source]¶
Attach a measurements database to one plate row, and say which.
rowis the row the user aimed the drop at – an EXPLICIT assignment, which is remembered and survives the re-proposal that the next dropped CSV triggers.With no row, the database is offered to the token pairing first, so
plate2/measurements/measurements.dbfinds plate 2 wherever that row happens to be. Only when nothing in its path names a plate does it fall back to the first row that has no database – and the returned sentence, also shown under the table, NAMES that row. A database attached to row 0 in silence is a plate’s measurements quietly credited to another plate.Returns the sentence, so a caller with a console logs the same words the user is reading.
- Parameters:
path – path to the measurements database, as a string or path-like object; an empty path raises
ValueError.
- dragEnterEvent(event)[source]¶
Accept a drag carrying files this table can take.
- Parameters:
event – the Qt drag event.
- dragMoveEvent(event)[source]¶
Keep accepting while droppable files stay over the table.
- Parameters:
event – the Qt drag event.
- dropEvent(event)[source]¶
Route each dropped file to the column, and row, it belongs in.
A drop over the score or count column goes there regardless of what the file looks like – the user aimed it. A drop anywhere else is sorted by header, so dropping the whole set at once still fills both columns correctly.
A DATABASE is decided by its extension and never by aim: a
.dbdropped on the score column is a mis-aim, not a request to fit the regression on a sqlite file, and a CSV dropped on the database column is likewise still a CSV. What aim adds for a database is the ROW – dropping it on a plate’s row attaches it to THAT plate, which is the one thing the token pairing cannot know when the file is calledmeasurements.dblike everybody else’s.- Parameters:
event – the drop event; its local file URLs and drop position are read, and it is ignored when it carries no usable paths.
- get_value() list[dict][source]¶
Every row, in the shape the settings dict wants.
- Returns:
one dict per plate row.
- missing_databases() list[source]¶
Rows whose database is not on disk:
(row number, plate, path).Row numbers are 1-based, the way the table numbers them.
Checked as soon as the path is attached and restated in the status line, because the alternative is a run that reads its inputs, fits nothing for four minutes, and then fails on a path the panel could have flagged the moment the settings were loaded. A settings file is routinely written on one machine and run on another, which is the case that produces this.
- set_value(value: Any) None[source]¶
Replace every row from a settings value.
SIGNALS BLOCKED while the rows are rebuilt: this is called when a settings file is poured in, and one change signal per cell would re-validate the whole form once per cell.
- Parameters:
value – the rows, as the settings dict carries them.
- showEvent(event)[source]¶
Take the Download row above over this table’s columns.
WHY THE TABLE REACHES UP FOR THE STRIP RATHER THAN THE OTHER WAY ROUND. The strip is built by the generic
AppScreen, which knows nothing about this table’s columns – it is four buttons in a QHBoxLayout, and every module’s example-data row is built the same way. This widget is the only object that knows what the columns ARE, and it is the only widget in the form that is Regression’s alone, so the knowledge and the alignment are kept in the same place.ON SHOW rather than in
__init__: the strip is added to the section AFTER the settings rows are, so at construction there is nothing to find, and the widget has no parent chain to walk up either. Once installed the layout follows the header on its own, and the call is idempotent, so repeated shows cost one dictionary lookup.- Parameters:
event – the show event; it is not inspected, only passed on to the base class first.
- spacr.qt.widgets.file_list.is_database_path(path) bool[source]¶
Truewhenpathnames a measurements database by extension.One rule, in one place, for the widget that files a dropped path into a column and for the drop handler that catches the same file when it lands on the screen around the widget. Two copies would disagree the first time somebody’s database was called
plate1.sqlite.- Parameters:
path – file path as a string or path-like object; its extension is compared, case-insensitively, with
DATABASE_EXTENSIONS.
- spacr.qt.widgets.file_list.side_for_header(path) str[source]¶
'count'when the file’s header names a gRNA and a count, else'score'.Read from the header rather than the filename: a count export carries a gRNA name and a count, a score export carries neither, and that is true whatever the file is called.
Module-level because two screens ask the same question of the same files. Regression asks it through
PairedFileTableWidget; Parameter Sweep holds its two sides in separate list widgets and asks it throughspacr.qt.dnd_handlers.SweepInputsDropHandler. A second copy of this rule would drift, and the direction it would drift in is silent: a count table filed as a score is not an error, it is a wrong regression.- Parameters:
path – CSV file whose first row is read as the header; a file that cannot be read counts as a score file.
- spacr.qt.widgets.file_list.suggest_file_pairs(scores: Sequence[str], counts: Sequence[str], *, databases: Sequence[str] = ()) list[dict][source]¶
Propose visible score/count/database rows by filename tokens.
A proposal is never authoritative: the editable table is the contract the user confirms. Unique best matches are used; ties remain unpaired.
databasesis keyword-only and defaults to nothing, so the two-argument call every existing caller makes still means what it did. A database is matched against BOTH cells of a row it may join – a plate is named by its score CSV as often as by its count CSV – and one that matches nothing is listed on its own row rather than being dropped or guessed onto row 0.- Parameters:
scores – score CSV paths; each starts a row, in the order given.
counts – count CSV paths; each is paired with the score whose filename tokens it uniquely matches best, unmatched ones fill score rows still missing a count, and any left over get rows of their own.
Nested helpers¶
- FilePathListWidget._follow_path_probes.let_go(*_args) None¶
Drop the probe connection as the widget is destroyed.
- Parameters:
_args – whatever
destroyedsends; unused.
spacr/qt/widgets/file_list.py:1594
- FilePathListWidget._follow_path_probes.redraw(_path: str, _answer: bool) None¶
Refresh the hint once a probe has an answer.
Both arguments are ignored: the hint is rebuilt from every path it shows, so which one answered does not change the work.
- Parameters:
_path – the path that was probed; unused.
_answer – what the probe found; unused.
spacr/qt/widgets/file_list.py:1577