From 49d3dff3382d2bb6b394892861361183798189bb Mon Sep 17 00:00:00 2001 From: JoeZiminski Date: Thu, 18 Dec 2025 16:45:59 +0000 Subject: [PATCH 1/5] Dynamically turn off transfer checkboxes. --- datashuttle/tui/screens/datatypes.py | 69 ++++++++++++++++++++++---- datashuttle/tui/tabs/create_folders.py | 4 +- datashuttle/tui/tabs/transfer.py | 6 +-- 3 files changed, 64 insertions(+), 15 deletions(-) diff --git a/datashuttle/tui/screens/datatypes.py b/datashuttle/tui/screens/datatypes.py index 7c21f84df..10dbc953a 100644 --- a/datashuttle/tui/screens/datatypes.py +++ b/datashuttle/tui/screens/datatypes.py @@ -213,7 +213,7 @@ class DatatypeCheckboxes(Static): def __init__( self, interface: Interface, - create_or_transfer: Literal["create", "transfer"] = "create", + tab_name: Literal["create", "transfer"] = "create", id: Optional[str] = None, ) -> None: """Initialise the DatatypeCheckboxes. @@ -223,7 +223,7 @@ def __init__( interface Datashuttle Interface object. - create_or_transfer + tab_name Whether we are on the "create" or "transfer" tab. id @@ -233,9 +233,9 @@ def __init__( super(DatatypeCheckboxes, self).__init__(id=id) self.interface = interface - self.create_or_transfer = create_or_transfer + self.tab_name = tab_name - self.settings_key = get_tui_settings_key_name(self.create_or_transfer) + self.settings_key = get_tui_settings_key_name(self.tab_name) # `datatype_config` is basically just a convenience wrapper # around interface.get_tui_settings @@ -249,7 +249,7 @@ def compose(self) -> ComposeResult: if setting["displayed"]: yield Checkbox( datatype.replace("_", " "), - id=get_checkbox_name(self.create_or_transfer, datatype), + id=get_checkbox_name(self.tab_name, datatype), value=setting["on"], ) @@ -264,7 +264,7 @@ def on_checkbox_changed(self) -> None: for datatype in self.datatype_config.keys(): if self.datatype_config[datatype]["displayed"]: self.datatype_config[datatype]["on"] = self.query_one( - f"#{get_checkbox_name(self.create_or_transfer, datatype)}" + f"#{get_checkbox_name(self.tab_name, datatype)}" ).value self.interface.save_tui_settings( @@ -276,7 +276,7 @@ def on_mount(self) -> None: for datatype in self.datatype_config.keys(): if self.datatype_config[datatype]["displayed"]: self.query_one( - f"#{get_checkbox_name(self.create_or_transfer, datatype)}" + f"#{get_checkbox_name(self.tab_name, datatype)}" ).tooltip = tooltips[datatype] def selected_datatypes(self) -> List[str]: @@ -289,22 +289,69 @@ def selected_datatypes(self) -> List[str]: return selected_datatypes +class TransferDatatypeCheckboxes(DatatypeCheckboxes): + """BE PLACEHOLDER.""" + + def __init__(self, interface, id): + """BE PLACEHOLDER.""" + super().__init__(interface, "transfer", id) + + @on(Checkbox.Changed) + def on_checkbox_changed(self, event: Checkbox.Changed) -> None: + """BE PLACEHOLDER.""" + checkbox = event.control + checkbox_name = get_datatype_from_checkbox_name(str(checkbox.label)) + + if checkbox.value: + all_datatypes = [ + dtype + for dtype in self.datatype_config.keys() + if dtype not in ["all", "all_datatype", "all_non_datatype"] + ] + + if checkbox_name == "all": + to_turn_off = [ + "all_datatype", + "all_non_datatype", + ] + all_datatypes + elif checkbox_name == "all_datatype": + to_turn_off = ["all"] + all_datatypes + elif checkbox_name == "all_non_datatype": + to_turn_off = ["all"] + else: + to_turn_off = ["all", "all_datatype"] + + for datatype in to_turn_off: + if self.datatype_config[datatype]["displayed"]: + self.query_one( + f"#{get_checkbox_name(self.tab_name, datatype)}" + ).value = False + + super().on_checkbox_changed() + + # Helpers # -------------------------------------------------------------------------------------- def get_checkbox_name( - create_or_transfer: Literal["create", "transfer"], datatype + tab_name: Literal["create", "transfer"], datatype ) -> str: """Return a canonical formatted checkbox name.""" - return f"{create_or_transfer}_{datatype}_checkbox" + return f"{tab_name}_{datatype}_checkbox" + + +def get_datatype_from_checkbox_name(checkbox_name: str) -> str: + """Get the datatype from the output of `get_checkbox_name()`.""" + split_datatype = checkbox_name.split("_")[1:-1] + return "_".join(split_datatype) def get_tui_settings_key_name( - create_or_transfer: Literal["create", "transfer"], + tab_name: Literal["create", "transfer"], ) -> str: """Return the canonical tui settings key.""" - if create_or_transfer == "create": + if tab_name == "create": settings_key = "create_checkboxes_on" else: settings_key = "transfer_checkboxes_on" diff --git a/datashuttle/tui/tabs/create_folders.py b/datashuttle/tui/tabs/create_folders.py index aa9422188..09daeb136 100644 --- a/datashuttle/tui/tabs/create_folders.py +++ b/datashuttle/tui/tabs/create_folders.py @@ -546,5 +546,7 @@ def update_directorytree_root(self, new_root_path: Path) -> None: def get_datatype_checkboxes_widget(self): """Create the datatype checkboxes, centralised as used in multiple places.""" return DatatypeCheckboxes( - self.interface, id="create_folders_datatype_checkboxes" + self.interface, + tab_name="create", + id="create_folders_datatype_checkboxes", ) diff --git a/datashuttle/tui/tabs/transfer.py b/datashuttle/tui/tabs/transfer.py index a6362ad85..75c38af64 100644 --- a/datashuttle/tui/tabs/transfer.py +++ b/datashuttle/tui/tabs/transfer.py @@ -32,8 +32,8 @@ TreeAndInputTab, ) from datashuttle.tui.screens.datatypes import ( - DatatypeCheckboxes, DisplayedDatatypesScreen, + TransferDatatypeCheckboxes, ) from datashuttle.tui.screens.modal_dialogs import ( ConfirmAndAwaitTransferPopup, @@ -424,9 +424,9 @@ def transfer_data(self) -> Worker[InterfaceOutput]: def get_datatypes_checkboxes_widget(self): """Create the datatype checkboxes, centralised as used in multiple places.""" - return DatatypeCheckboxes( + return TransferDatatypeCheckboxes( self.interface, - create_or_transfer="transfer", + # tab_name="transfer", id="transfer_custom_datatype_checkboxes", ) From 5df57641ebf09e75916acd43292c6e415f711cff Mon Sep 17 00:00:00 2001 From: JoeZiminski Date: Thu, 18 Dec 2025 18:24:34 +0000 Subject: [PATCH 2/5] Add tests. --- datashuttle/tui/screens/datatypes.py | 2 +- .../test_tui_widgets_and_defaults.py | 55 ++++++++++++++++++- 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/datashuttle/tui/screens/datatypes.py b/datashuttle/tui/screens/datatypes.py index 10dbc953a..672201bde 100644 --- a/datashuttle/tui/screens/datatypes.py +++ b/datashuttle/tui/screens/datatypes.py @@ -300,7 +300,7 @@ def __init__(self, interface, id): def on_checkbox_changed(self, event: Checkbox.Changed) -> None: """BE PLACEHOLDER.""" checkbox = event.control - checkbox_name = get_datatype_from_checkbox_name(str(checkbox.label)) + checkbox_name = get_datatype_from_checkbox_name(str(checkbox.id)) if checkbox.value: all_datatypes = [ diff --git a/tests/tests_tui/test_tui_widgets_and_defaults.py b/tests/tests_tui/test_tui_widgets_and_defaults.py index c95c7e9d7..3de2091b2 100644 --- a/tests/tests_tui/test_tui_widgets_and_defaults.py +++ b/tests/tests_tui/test_tui_widgets_and_defaults.py @@ -1075,14 +1075,15 @@ async def test_all_checkboxes(self, setup_project_paths): "ephys", "funcimg", "anat", - "all", - "all_datatype", "all_non_datatype", + # "all" and "all_datatype" will turn off transfer datatypes + # and are tested separately in `test_transfer_checkboxes_dynamic_on_off()`. ]: await self.change_checkbox( pilot, f"#transfer_{datatype}_checkbox" ) expected_transfer[datatype]["on"] = True + self.check_datatype_checkboxes( pilot, "transfer", expected_transfer ) @@ -1103,6 +1104,56 @@ async def test_all_checkboxes(self, setup_project_paths): await pilot.pause() + @pytest.mark.asyncio + async def test_transfer_checkboxes_dynamic_on_off( + self, setup_project_paths, mocker + ): + tmp_config_path, tmp_path, project_name = setup_project_paths.values() + + app = TuiApp() + async with app.run_test(size=self.tui_size()) as pilot: + # Set up the TUI on the 'transfer' tab (custom) and + # open the datatype selection screen + await self.check_and_click_onto_existing_project( + pilot, project_name + ) + + await self.switch_tab(pilot, "transfer") + await self.scroll_to_click_pause( + pilot, "#transfer_custom_radiobutton" + ) + + def load_dict(): + return pilot.app.screen.interface.project._load_persistent_settings()[ + "tui" + ]["transfer_checkboxes_on"] + + assert load_dict()["all"]["on"] + await self.change_checkbox(pilot, "#transfer_behav_checkbox") + assert not load_dict()["all"]["on"] + + await self.change_checkbox( + pilot, "#transfer_all_non_datatype_checkbox" + ) + assert load_dict()["all_non_datatype"]["on"] + assert load_dict()["behav"]["on"] + + await self.change_checkbox( + pilot, "#transfer_all_datatype_checkbox" + ) + assert load_dict()["all_datatype"]["on"] + assert not load_dict()["behav"]["on"] + + await self.change_checkbox(pilot, "#transfer_all_checkbox") + assert load_dict()["all"]["on"] + assert not load_dict()["all_datatype"]["on"] + assert not load_dict()["all_non_datatype"]["on"] + + await self.change_checkbox( + pilot, "#transfer_all_non_datatype_checkbox" + ) + assert not load_dict()["all"]["on"] + def check_datatype_checkboxes(self, pilot, tab, expected_on): assert tab in ["create", "transfer"] if tab == "create": From 1edd6976e3f7c3b226eab200d0e4136efe358485 Mon Sep 17 00:00:00 2001 From: JoeZiminski Date: Thu, 18 Dec 2025 20:28:24 +0000 Subject: [PATCH 3/5] Add docstrings. --- datashuttle/tui/screens/datatypes.py | 34 +++++++++++++------ .../test_tui_widgets_and_defaults.py | 6 ++++ 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/datashuttle/tui/screens/datatypes.py b/datashuttle/tui/screens/datatypes.py index 672201bde..0457a59a3 100644 --- a/datashuttle/tui/screens/datatypes.py +++ b/datashuttle/tui/screens/datatypes.py @@ -185,13 +185,6 @@ def on_selection_list_selection_toggled( class DatatypeCheckboxes(Static): """Dynamically-populated checkbox widget for convenient datatype selection. - Parameters - ---------- - settings_key - 'create' if datatype checkboxes for the create tab, - 'transfer' for the transfer tab. Transfer tab includes - additional datatype options (e.g. "all"). - Attributes ---------- datatype_config @@ -290,15 +283,36 @@ def selected_datatypes(self) -> List[str]: class TransferDatatypeCheckboxes(DatatypeCheckboxes): - """BE PLACEHOLDER.""" + """Subclass of the data type checkboxes class for the transfer tab. + + This subclass extends `on_checkbox_changed` by + + """ def __init__(self, interface, id): - """BE PLACEHOLDER.""" + """Initialise TransferDatatypeCheckboxes. + + Parameters + ---------- + interface + Datashuttle Interface object. + + id + Textual ID for the DatatypeCheckboxes widget. + + """ super().__init__(interface, "transfer", id) @on(Checkbox.Changed) def on_checkbox_changed(self, event: Checkbox.Changed) -> None: - """BE PLACEHOLDER.""" + """Dynamically turn off checkboxes depending on the activated checkbox then save. + + In the transfer tab, we have a few different checkboxes that + are mutually exclusive. For example, `all` and `all_datatypes` + are redundant. This function turns off checkboxes redundant + with the selected checkbox, before calling the super class + which saves the state of the currently selected checkboxes. + """ checkbox = event.control checkbox_name = get_datatype_from_checkbox_name(str(checkbox.id)) diff --git a/tests/tests_tui/test_tui_widgets_and_defaults.py b/tests/tests_tui/test_tui_widgets_and_defaults.py index 3de2091b2..8337dc2df 100644 --- a/tests/tests_tui/test_tui_widgets_and_defaults.py +++ b/tests/tests_tui/test_tui_widgets_and_defaults.py @@ -1108,6 +1108,12 @@ async def test_all_checkboxes(self, setup_project_paths): async def test_transfer_checkboxes_dynamic_on_off( self, setup_project_paths, mocker ): + """ + This tests that mutually exclusive checkbox options turn + each other off / on when set. This is necessary for transfer + tab custom datatypes in which some checkboxes (e.g. "all") + should not be selected with other (e.g. "behav"). + """ tmp_config_path, tmp_path, project_name = setup_project_paths.values() app = TuiApp() From 23ad4d7d1e609bc7a9cd4e3bfa1c35c5e83efff3 Mon Sep 17 00:00:00 2001 From: JoeZiminski Date: Fri, 9 Jan 2026 14:50:46 +0000 Subject: [PATCH 4/5] Small fixes and tidy ups. --- datashuttle/tui/screens/datatypes.py | 6 ++++-- datashuttle/tui/tabs/transfer.py | 1 - tests/tests_tui/test_tui_widgets_and_defaults.py | 10 ++++++++-- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/datashuttle/tui/screens/datatypes.py b/datashuttle/tui/screens/datatypes.py index 0457a59a3..2f92b483d 100644 --- a/datashuttle/tui/screens/datatypes.py +++ b/datashuttle/tui/screens/datatypes.py @@ -247,7 +247,7 @@ def compose(self) -> ComposeResult: ) @on(Checkbox.Changed) - def on_checkbox_changed(self) -> None: + def on_checkbox_changed(self, event: Checkbox.Changed) -> None: """Handle a datatype checkbox change. When a checkbox is changed, update the `self.datatype_config` @@ -285,7 +285,9 @@ def selected_datatypes(self) -> List[str]: class TransferDatatypeCheckboxes(DatatypeCheckboxes): """Subclass of the data type checkboxes class for the transfer tab. - This subclass extends `on_checkbox_changed` by + This subclass extends `on_checkbox_changed` by adding logic to + dynamically turn off mutually exclusive checkboxes when one is + selected, before delegating to the base implementation. """ diff --git a/datashuttle/tui/tabs/transfer.py b/datashuttle/tui/tabs/transfer.py index 75c38af64..d2af4e4ee 100644 --- a/datashuttle/tui/tabs/transfer.py +++ b/datashuttle/tui/tabs/transfer.py @@ -426,7 +426,6 @@ def get_datatypes_checkboxes_widget(self): """Create the datatype checkboxes, centralised as used in multiple places.""" return TransferDatatypeCheckboxes( self.interface, - # tab_name="transfer", id="transfer_custom_datatype_checkboxes", ) diff --git a/tests/tests_tui/test_tui_widgets_and_defaults.py b/tests/tests_tui/test_tui_widgets_and_defaults.py index 8337dc2df..a46389d4b 100644 --- a/tests/tests_tui/test_tui_widgets_and_defaults.py +++ b/tests/tests_tui/test_tui_widgets_and_defaults.py @@ -1106,13 +1106,13 @@ async def test_all_checkboxes(self, setup_project_paths): @pytest.mark.asyncio async def test_transfer_checkboxes_dynamic_on_off( - self, setup_project_paths, mocker + self, setup_project_paths ): """ This tests that mutually exclusive checkbox options turn each other off / on when set. This is necessary for transfer tab custom datatypes in which some checkboxes (e.g. "all") - should not be selected with other (e.g. "behav"). + should not be selected with other (e.g. "behav"). """ tmp_config_path, tmp_path, project_name = setup_project_paths.values() @@ -1129,32 +1129,38 @@ async def test_transfer_checkboxes_dynamic_on_off( pilot, "#transfer_custom_radiobutton" ) + # Create a function to reload the settings dict, refreshing the contents def load_dict(): return pilot.app.screen.interface.project._load_persistent_settings()[ "tui" ]["transfer_checkboxes_on"] + # turn on "behav" checkbox, check "all" is turned off assert load_dict()["all"]["on"] await self.change_checkbox(pilot, "#transfer_behav_checkbox") assert not load_dict()["all"]["on"] + # Turn on "all_non_datatype" checkbox, check "behav" is kept on await self.change_checkbox( pilot, "#transfer_all_non_datatype_checkbox" ) assert load_dict()["all_non_datatype"]["on"] assert load_dict()["behav"]["on"] + # Turn on "all_datatype" checkbox, check "behav" is turned off await self.change_checkbox( pilot, "#transfer_all_datatype_checkbox" ) assert load_dict()["all_datatype"]["on"] assert not load_dict()["behav"]["on"] + # Turn on "all" checkbox and check all_non_datatype and all_datatype are switched off await self.change_checkbox(pilot, "#transfer_all_checkbox") assert load_dict()["all"]["on"] assert not load_dict()["all_datatype"]["on"] assert not load_dict()["all_non_datatype"]["on"] + # Turn on "all_non_datatype" and check "all" is now off await self.change_checkbox( pilot, "#transfer_all_non_datatype_checkbox" ) From a2c2532910d3e42a798d97d66b339561cb11ed6b Mon Sep 17 00:00:00 2001 From: JoeZiminski Date: Mon, 12 Jan 2026 17:22:57 +0000 Subject: [PATCH 5/5] Fix tests. --- datashuttle/tui/screens/datatypes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/datashuttle/tui/screens/datatypes.py b/datashuttle/tui/screens/datatypes.py index 2f92b483d..c319a479a 100644 --- a/datashuttle/tui/screens/datatypes.py +++ b/datashuttle/tui/screens/datatypes.py @@ -343,7 +343,7 @@ def on_checkbox_changed(self, event: Checkbox.Changed) -> None: f"#{get_checkbox_name(self.tab_name, datatype)}" ).value = False - super().on_checkbox_changed() + super().on_checkbox_changed(event) # Helpers