Skip to content

Commit d331b2d

Browse files
authored
Allow tags in name template regexp. (#404)
* Allow tags in name template regexp. * Add tests. * Update documentation.
1 parent 69e0b4c commit d331b2d

File tree

5 files changed

+140
-4
lines changed

5 files changed

+140
-4
lines changed

datashuttle/utils/formatting.py

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -237,13 +237,28 @@ def update_names_with_datetime(names: List[str]) -> None:
237237
Format using key-value pair for bids, i.e. date-20221223_time-
238238
"""
239239
date = str(datetime.datetime.now().date().strftime("%Y%m%d"))
240-
date_with_key = f"date-{date}"
240+
date_with_key = format_date(date)
241241

242242
time_ = datetime.datetime.now().time().strftime("%H%M%S")
243-
time_with_key = f"time-{time_}"
243+
time_with_key = format_time(time_)
244244

245-
datetime_with_key = f"datetime-{date}T{time_}"
245+
datetime_with_key = format_datetime(date, time_)
246246

247+
replace_date_time_tags_in_name(
248+
names, datetime_with_key, date_with_key, time_with_key
249+
)
250+
251+
252+
def replace_date_time_tags_in_name(
253+
names: List[str],
254+
datetime_with_key: str,
255+
date_with_key: str,
256+
time_with_key: str,
257+
):
258+
"""
259+
For all names in the list, do the replacement of tags
260+
with their final values.
261+
"""
247262
for i, name in enumerate(names):
248263
# datetime conditional must come first.
249264
if tags("datetime") in name:
@@ -261,6 +276,18 @@ def update_names_with_datetime(names: List[str]) -> None:
261276
names[i] = name.replace(tags("time"), time_with_key)
262277

263278

279+
def format_date(date: str) -> str:
280+
return f"date-{date}"
281+
282+
283+
def format_time(time_: str) -> str:
284+
return f"time-{time_}"
285+
286+
287+
def format_datetime(date: str, time_: str) -> str:
288+
return f"datetime-{date}T{time_}"
289+
290+
264291
def add_underscore_before_after_if_not_there(string: str, key: str) -> str:
265292
"""
266293
If names are passed with @DATE@, @TIME@, or @DATETIME@

datashuttle/utils/validation.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from itertools import chain
1111

1212
from datashuttle.configs import canonical_folders
13-
from datashuttle.utils import getters, utils
13+
from datashuttle.utils import formatting, getters, utils
1414
from datashuttle.utils.custom_exceptions import NeuroBlueprintError
1515

1616
# -----------------------------------------------------------------------------
@@ -102,6 +102,8 @@ def names_dont_match_templates(
102102
if regexp is None:
103103
return False, f"No template set for prefix: {prefix}"
104104

105+
regexp = replace_tags_in_regexp(regexp)
106+
105107
bad_names = []
106108
for name in names_list:
107109
if not re.fullmatch(regexp, name):
@@ -119,6 +121,29 @@ def names_dont_match_templates(
119121
return False, ""
120122

121123

124+
def replace_tags_in_regexp(regexp: str) -> str:
125+
"""
126+
Before validation, all tags in the names are converted to
127+
their final values (e.g. @DATE@ -> _date-<date>). We also want to
128+
allow template to be formatted like `sub-\d\d_@DATE@` as it
129+
is convenient for auto-completion in the TUI.
130+
131+
Therefore we must replace the tags in the regexp with their
132+
actual regexp equivalent before comparison.
133+
Note `replace_date_time_tags_in_name()` operates in place on a list.
134+
"""
135+
regexp_list = [regexp]
136+
date_regexp = "\d\d\d\d\d\d\d\d"
137+
time_regexp = "\d\d\d\d\d\d"
138+
formatting.replace_date_time_tags_in_name(
139+
regexp_list,
140+
datetime_with_key=formatting.format_datetime(date_regexp, time_regexp),
141+
date_with_key=formatting.format_date(date_regexp),
142+
time_with_key=formatting.format_time(time_regexp),
143+
)
144+
return regexp_list[0]
145+
146+
122147
def get_names_format(bad_names):
123148
"""
124149
A convenience function to properly format error messages

docs/source/pages/how_tos/use-name-templates.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ as a regexp where `\d` stands for 'any digit`:
2323

2424
If this is defined as a Name Template, any name that
2525
does not take this form will result in a validation error.
26+
Name templates can include [convenience tags](create-folders-convenience-tags).
27+
(`@DATE@`, `@TIME@` or `@DATETIME@`.)
2628

2729
## Set up Name Templates
2830
::::{tab-set}

tests/tests_integration/test_validation.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -647,3 +647,57 @@ def test_validate_names_against_project_interactions(self, project):
647647
"the same ses id as ses-003_id-random. "
648648
"The existing folder is ses-003." in str(w[1].message)
649649
)
650+
651+
def test_tags_in_name_templates_pass_validation(self, project):
652+
"""
653+
It is useful to allow tags in the `name_templates` as it means
654+
auto-completion in the TUI can use tags for automatic name
655+
generation. Because all subject and session names are
656+
fully formatted (e.g. @DATE@ converted to actual dates)
657+
prior to validation, the regexp must also have @DATE@
658+
and other tags with their regexp equivalent. Check
659+
this behaviour here.
660+
"""
661+
name_templates = {
662+
"on": True,
663+
"sub": "sub-\d\d_@DATE@",
664+
"ses": "ses-\d\d\d@DATETIME@",
665+
}
666+
667+
project.set_name_templates(name_templates)
668+
669+
# Standard behaviour, should not raise
670+
project.create_folders(
671+
"rawdata",
672+
"sub-01_date-20240101",
673+
"ses-001_datetime-20240101T142323",
674+
)
675+
# added tags, should not raise
676+
project.create_folders("rawdata", "sub-02@DATE@", "ses-001_@DATETIME@")
677+
678+
# break the name template validation, for sub, should raise
679+
with pytest.raises(NeuroBlueprintError):
680+
project.create_folders("rawdata", "sub-03_date_202401")
681+
682+
# break the name template validation, for ses, should raise
683+
with pytest.raises(NeuroBlueprintError):
684+
project.create_folders(
685+
"rawdata", "sub-03_date_20240101", "ses-001_date-202401"
686+
)
687+
688+
# Do a quick test for tim
689+
name_templates["sub"] = "sub-\d\d_@TIME@"
690+
project.set_name_templates(name_templates)
691+
692+
# use time tag, should not raise
693+
project.create_folders(
694+
"rawdata",
695+
"sub-03@TIME@",
696+
)
697+
698+
with pytest.raises(NeuroBlueprintError):
699+
# use misspelled time tag, should raise
700+
project.create_folders(
701+
"rawdata",
702+
"sub-03_mime_010101",
703+
)

tests/tests_unit/test_validation_unit.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,3 +225,31 @@ def test_new_name_duplicates_existing(self, prefix):
225225
f"A {prefix} already exists with the same {prefix} id as {prefix}-3. "
226226
f"The existing folder is {prefix}-3_s-a." in message
227227
)
228+
229+
def test_tags_autoreplace_in_regexp(self):
230+
"""
231+
Check the validation function `replace_tags_in_regexp()`
232+
correctly replaces tags in a regexp with their regexp equivalent.
233+
234+
Test date, time and datetime with some random regexp that
235+
implicitly check a few other cases (e.g. underscore filling around
236+
the tag).
237+
"""
238+
date_regexp = r"sub-\d\d@DATE@_some-tag"
239+
fixed_date_regexp = validation.replace_tags_in_regexp(date_regexp)
240+
assert fixed_date_regexp == r"sub-\d\d_date-\d\d\d\d\d\d\d\d_some-tag"
241+
242+
time_regexp = r"ses-\d\d\d\d@TIME@_some-.?.?tag"
243+
fixed_time_regexp = validation.replace_tags_in_regexp(time_regexp)
244+
assert (
245+
fixed_time_regexp == r"ses-\d\d\d\d_time-\d\d\d\d\d\d_some-.?.?tag"
246+
)
247+
248+
datetime_regexp = r"ses-.?.?.?@DATETIME@some-.?.?tag"
249+
fixed_datetime_regexp = validation.replace_tags_in_regexp(
250+
datetime_regexp
251+
)
252+
assert (
253+
fixed_datetime_regexp
254+
== r"ses-.?.?.?_datetime-\d\d\d\d\d\d\d\dT\d\d\d\d\d\d_some-.?.?tag"
255+
)

0 commit comments

Comments
 (0)