Skip to content

Commit 094696d

Browse files
authored
Handle bad project names (#527)
* Do not allow empty project name. * Handle project names that begin with number. * Add empty project name tests. * Add tests. * Fix test. * Apply review feedback.
1 parent 20dd9b7 commit 094696d

File tree

5 files changed

+77
-5
lines changed

5 files changed

+77
-5
lines changed

datashuttle/datashuttle_class.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1364,6 +1364,10 @@ def _error_on_base_project_name(self, project_name):
13641364
"The project name must contain alphanumeric characters only.",
13651365
ValueError,
13661366
)
1367+
if project_name == "":
1368+
utils.log_and_raise_error(
1369+
"The project name cannot be empty.", NeuroBlueprintError
1370+
)
13671371

13681372
def _log_successful_config_change(self, message: bool = False) -> None:
13691373
"""Log the entire config at the time of config change.

datashuttle/tui/screens/project_selector.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,15 +50,18 @@ def compose(self) -> ComposeResult:
5050
yield Header(id="project_select_header")
5151
yield Button("Main Menu", id="all_main_menu_buttons")
5252
yield Container(
53-
*[Button(name, id=name) for name in self.project_names],
53+
*[
54+
Button(name, id=self.name_to_id(name))
55+
for name in self.project_names
56+
],
5457
id="project_select_top_container",
5558
)
5659

5760
def on_button_pressed(self, event: Button.Pressed) -> None:
5861
"""Handle a button press on ProjectSelectorScreen."""
59-
if event.button.id in self.project_names:
60-
project_name = event.button.id
62+
project_name = self.id_to_name(event.button.id)
6163

64+
if project_name in self.project_names:
6265
interface = Interface()
6366
success, output = interface.select_existing_project(project_name)
6467

@@ -69,3 +72,17 @@ def on_button_pressed(self, event: Button.Pressed) -> None:
6972

7073
elif event.button.id == "all_main_menu_buttons":
7174
self.dismiss(False)
75+
76+
@staticmethod
77+
def name_to_id(name: str):
78+
"""Convert the project name to a textual ID.
79+
80+
Textual ids cannot start with a number, so ensure
81+
all ids are prefixed with text instead of the project name.
82+
"""
83+
return f"safety_prefix_{name}"
84+
85+
@staticmethod
86+
def id_to_name(id: str):
87+
"""See `name_to_id()`."""
88+
return id[len("safety_prefix_") :]

tests/tests_integration/test_configs.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@
66

77
from datashuttle import DataShuttle
88
from datashuttle.utils import getters
9-
from datashuttle.utils.custom_exceptions import ConfigError
9+
from datashuttle.utils.custom_exceptions import (
10+
ConfigError,
11+
NeuroBlueprintError,
12+
)
1013

1114

1215
class TestConfigs(BaseTest):
@@ -45,6 +48,17 @@ def test_warning_on_startup(self, no_cfg_project):
4548
"Use make_config_file() to setup before continuing."
4649
)
4750

51+
def test_empty_project_name(self, tmp_path):
52+
"""
53+
Empty project names ("") are not allowed.
54+
"""
55+
os.chdir(tmp_path) # avoids messy backup folder creation
56+
57+
with pytest.raises(NeuroBlueprintError) as e:
58+
DataShuttle("")
59+
60+
assert str(e.value) == "The project name cannot be empty."
61+
4862
@pytest.mark.parametrize(
4963
"bad_pattern",
5064
[

tests/tests_tui/test_tui_configs.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,43 @@ async def test_bad_configs_screen_input(self, empty_project_paths):
360360
)
361361
await pilot.pause()
362362

363+
# -------------------------------------------------------------------------
364+
# Test project name is number
365+
# -------------------------------------------------------------------------
366+
367+
@pytest.mark.asyncio
368+
async def test_project_name_is_number(self, empty_project_paths):
369+
"""
370+
Make a project that has a number name, and check the project screen
371+
can be loaded.
372+
"""
373+
app = TuiApp()
374+
async with app.run_test(size=self.tui_size()) as pilot:
375+
# Set up a project with a numerical project name
376+
project_name = "123"
377+
378+
await self.scroll_to_click_pause(
379+
pilot, "#mainwindow_new_project_button"
380+
)
381+
382+
await self.fill_input(pilot, "#configs_name_input", project_name)
383+
await self.fill_input(pilot, "#configs_local_path_input", "a")
384+
await self.fill_input(pilot, "#configs_central_path_input", "b")
385+
await self.scroll_to_click_pause(
386+
pilot, "#configs_save_configs_button"
387+
)
388+
389+
# Go back to main menu and load the project screen
390+
await self.close_messagebox(pilot)
391+
392+
await self.scroll_to_click_pause(pilot, "#all_main_menu_buttons")
393+
394+
await self.check_and_click_onto_existing_project(
395+
pilot, project_name
396+
)
397+
398+
await pilot.pause()
399+
363400
# -------------------------------------------------------------------------
364401
# Helpers
365402
# -------------------------------------------------------------------------

tests/tests_tui/tui_base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ async def check_and_click_onto_existing_project(self, pilot, project_name):
166166
assert len(pilot.app.screen.project_names) == 1
167167
assert project_name in pilot.app.screen.project_names
168168

169-
await pilot.click(f"#{project_name}")
169+
await pilot.click(f"#safety_prefix_{project_name}")
170170
await pilot.pause()
171171

172172
assert isinstance(pilot.app.screen, ProjectManagerScreen)

0 commit comments

Comments
 (0)