junifer 0.0.4.dev782__py3-none-any.whl → 0.0.4.dev810__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. junifer/_version.py +2 -2
  2. junifer/configs/juseless/datagrabbers/aomic_id1000_vbm.py +6 -1
  3. junifer/configs/juseless/datagrabbers/camcan_vbm.py +6 -1
  4. junifer/configs/juseless/datagrabbers/ixi_vbm.py +6 -1
  5. junifer/configs/juseless/datagrabbers/ucla.py +42 -24
  6. junifer/configs/juseless/datagrabbers/ukb_vbm.py +6 -1
  7. junifer/datagrabber/aomic/id1000.py +98 -91
  8. junifer/datagrabber/aomic/piop1.py +97 -73
  9. junifer/datagrabber/aomic/piop2.py +97 -73
  10. junifer/datagrabber/base.py +6 -4
  11. junifer/datagrabber/datalad_base.py +0 -2
  12. junifer/datagrabber/dmcc13_benchmark.py +87 -50
  13. junifer/datagrabber/hcp1200/hcp1200.py +21 -19
  14. junifer/datagrabber/pattern.py +124 -25
  15. junifer/datagrabber/pattern_datalad.py +111 -13
  16. junifer/datagrabber/tests/test_base.py +0 -6
  17. junifer/datagrabber/tests/test_datagrabber_utils.py +204 -76
  18. junifer/datagrabber/tests/test_datalad_base.py +0 -6
  19. junifer/datagrabber/tests/test_multiple.py +43 -10
  20. junifer/datagrabber/tests/test_pattern.py +125 -178
  21. junifer/datagrabber/tests/test_pattern_datalad.py +44 -25
  22. junifer/datagrabber/utils.py +141 -21
  23. junifer/datareader/default.py +6 -7
  24. {junifer-0.0.4.dev782.dist-info → junifer-0.0.4.dev810.dist-info}/METADATA +1 -1
  25. {junifer-0.0.4.dev782.dist-info → junifer-0.0.4.dev810.dist-info}/RECORD +30 -30
  26. {junifer-0.0.4.dev782.dist-info → junifer-0.0.4.dev810.dist-info}/AUTHORS.rst +0 -0
  27. {junifer-0.0.4.dev782.dist-info → junifer-0.0.4.dev810.dist-info}/LICENSE.md +0 -0
  28. {junifer-0.0.4.dev782.dist-info → junifer-0.0.4.dev810.dist-info}/WHEEL +0 -0
  29. {junifer-0.0.4.dev782.dist-info → junifer-0.0.4.dev810.dist-info}/entry_points.txt +0 -0
  30. {junifer-0.0.4.dev782.dist-info → junifer-0.0.4.dev810.dist-info}/top_level.txt +0 -0
@@ -32,11 +32,98 @@ class PatternDataGrabber(BaseDataGrabber):
32
32
  types : list of str
33
33
  The types of data to be grabbed.
34
34
  patterns : dict
35
- Patterns for each type of data as a dictionary. The keys are the types
36
- and the values are the patterns. Each occurrence of the string
37
- ``{subject}`` in the pattern will be replaced by the indexed element.
35
+ Data type patterns as a dictionary. It has the following schema:
36
+
37
+ * ``"T1w"`` :
38
+
39
+ .. code-block:: none
40
+
41
+ {
42
+ "mandatory": ["pattern", "space"],
43
+ "optional": []
44
+ }
45
+
46
+ * ``"T2w"`` :
47
+
48
+ .. code-block:: none
49
+
50
+ {
51
+ "mandatory": ["pattern", "space"],
52
+ "optional": []
53
+ }
54
+
55
+ * ``"BOLD"`` :
56
+
57
+ .. code-block:: none
58
+
59
+ {
60
+ "mandatory": ["pattern", "space"],
61
+ "optional": ["mask_item"]
62
+ }
63
+
64
+ * ``"Warp"`` :
65
+
66
+ .. code-block:: none
67
+
68
+ {
69
+ "mandatory": ["pattern", "src", "dst"],
70
+ "optional": []
71
+ }
72
+
73
+ * ``"BOLD_confounds"`` :
74
+
75
+ .. code-block:: none
76
+
77
+ {
78
+ "mandatory": ["pattern", "format"],
79
+ "optional": []
80
+ }
81
+
82
+ * ``"VBM_GM"`` :
83
+
84
+ .. code-block:: none
85
+
86
+ {
87
+ "mandatory": ["pattern", "space"],
88
+ "optional": []
89
+ }
90
+
91
+ * ``"VBM_WM"`` :
92
+
93
+ .. code-block:: none
94
+
95
+ {
96
+ "mandatory": ["pattern", "space"],
97
+ "optional": []
98
+ }
99
+
100
+ Basically, for each data type, one needs to provide ``mandatory`` keys
101
+ and can choose to also provide ``optional`` keys. The value for each
102
+ key is a string. So, one needs to provide necessary data types as a
103
+ dictionary, for example:
104
+
105
+ .. code-block:: none
106
+
107
+ {
108
+ "BOLD": {
109
+ "pattern": "...",
110
+ "space": "...",
111
+ },
112
+ "T1w": {
113
+ "pattern": "...",
114
+ "space": "...",
115
+ },
116
+ "Warp": {
117
+ "pattern": "...",
118
+ "src": "...",
119
+ "dst": "...",
120
+ }
121
+ }
122
+
123
+ taken from :class:`.HCP1200`.
38
124
  replacements : str or list of str
39
- Replacements in the patterns for each item in the "element" tuple.
125
+ Replacements in the ``pattern`` key of each data type. The value needs
126
+ to be a list of all possible replacements.
40
127
  datadir : str or pathlib.Path
41
128
  The directory where the data is / will be stored.
42
129
  confounds_format : {"fmriprep", "adhoc"} or None, optional
@@ -52,7 +139,7 @@ class PatternDataGrabber(BaseDataGrabber):
52
139
  def __init__(
53
140
  self,
54
141
  types: List[str],
55
- patterns: Dict[str, str],
142
+ patterns: Dict[str, Dict[str, str]],
56
143
  replacements: Union[List[str], str],
57
144
  datadir: Union[str, Path],
58
145
  confounds_format: Optional[str] = None,
@@ -69,7 +156,10 @@ class PatternDataGrabber(BaseDataGrabber):
69
156
  self.replacements = replacements
70
157
 
71
158
  # Validate confounds format
72
- if confounds_format and confounds_format not in _CONFOUNDS_FORMATS:
159
+ if (
160
+ confounds_format is not None
161
+ and confounds_format not in _CONFOUNDS_FORMATS
162
+ ):
73
163
  raise_error(
74
164
  "Invalid value for `confounds_format`, should be one of "
75
165
  f"{_CONFOUNDS_FORMATS}."
@@ -143,6 +233,11 @@ class PatternDataGrabber(BaseDataGrabber):
143
233
  str
144
234
  The pattern with the element replaced.
145
235
 
236
+ Raises
237
+ ------
238
+ ValueError
239
+ If element keys do not match with replacements.
240
+
146
241
  """
147
242
  if list(element.keys()) != self.replacements:
148
243
  raise_error(
@@ -167,7 +262,7 @@ class PatternDataGrabber(BaseDataGrabber):
167
262
  return self.replacements
168
263
 
169
264
  def get_item(self, **element: str) -> Dict[str, Dict]:
170
- """Implement single element indexing in the database.
265
+ """Implement single element indexing for the datagrabber.
171
266
 
172
267
  This method constructs a real path to the requested item's data, by
173
268
  replacing the ``patterns`` with actual values passed via ``**element``.
@@ -184,20 +279,33 @@ class PatternDataGrabber(BaseDataGrabber):
184
279
  Dictionary of dictionaries for each type of data required for the
185
280
  specified element.
186
281
 
282
+ Raises
283
+ ------
284
+ RuntimeError
285
+ If more than one file matches for a data type's pattern or
286
+ if no file matches for a data type's pattern or
287
+ if file cannot be accessed for an element.
288
+
187
289
  """
188
290
  out = {}
189
291
  for t_type in self.types:
190
292
  t_pattern = self.patterns[t_type]
191
- t_replace = self._replace_patterns_glob(element, t_pattern)
293
+ t_replace = self._replace_patterns_glob(
294
+ element, t_pattern["pattern"]
295
+ )
192
296
  if "*" in t_replace:
193
297
  t_matches = list(self.datadir.absolute().glob(t_replace))
194
298
  if len(t_matches) > 1:
195
299
  raise_error(
196
300
  f"More than one file matches for {element} / {t_type}:"
197
- f" {t_matches}"
301
+ f" {t_matches}",
302
+ klass=RuntimeError,
198
303
  )
199
304
  elif len(t_matches) == 0:
200
- raise_error(f"No file matches for {element} / {t_type}")
305
+ raise_error(
306
+ f"No file matches for {element} / {t_type}",
307
+ klass=RuntimeError,
308
+ )
201
309
  t_out = t_matches[0]
202
310
  else:
203
311
  t_out = self.datadir / t_replace
@@ -205,22 +313,13 @@ class PatternDataGrabber(BaseDataGrabber):
205
313
  if not t_out.exists() and not t_out.is_symlink():
206
314
  raise_error(
207
315
  f"Cannot access {t_type} for {element}: "
208
- f"File {t_out} does not exist"
316
+ f"File {t_out} does not exist",
317
+ klass=RuntimeError,
209
318
  )
210
319
  # Update path for the element
211
- out[t_type] = {"path": t_out}
212
- # Update confounds format for BOLD_confounds
213
- # (if found in the datagrabber)
214
- if t_type == "BOLD_confounds":
215
- if not self.confounds_format:
216
- raise_error(
217
- "`confounds_format` needs to be one of "
218
- f"{_CONFOUNDS_FORMATS}, None provided. "
219
- "As the DataGrabber used specifies "
220
- "'BOLD_confounds', None is invalid."
221
- )
222
- # Set the format
223
- out[t_type].update({"format": self.confounds_format})
320
+ out[t_type] = t_pattern.copy() # copy data type dictionary
321
+ out[t_type].pop("pattern") # remove pattern key
322
+ out[t_type].update({"path": t_out}) # add path key
224
323
 
225
324
  return out
226
325
 
@@ -259,7 +358,7 @@ class PatternDataGrabber(BaseDataGrabber):
259
358
  re_pattern,
260
359
  glob_pattern,
261
360
  t_replacements,
262
- ) = self._replace_patterns_regex(t_pattern)
361
+ ) = self._replace_patterns_regex(t_pattern["pattern"])
263
362
  for fname in self.datadir.glob(glob_pattern):
264
363
  suffix = fname.relative_to(self.datadir).as_posix()
265
364
  m = re.match(re_pattern, suffix)
@@ -5,12 +5,11 @@
5
5
  # Synchon Mandal <s.mandal@fz-juelich.de>
6
6
  # License: AGPL
7
7
 
8
- from typing import Dict, List
9
8
 
10
9
  from ..api.decorators import register_datagrabber
10
+ from ..utils import logger
11
11
  from .datalad_base import DataladDataGrabber
12
12
  from .pattern import PatternDataGrabber
13
- from .utils import validate_patterns
14
13
 
15
14
 
16
15
  @register_datagrabber
@@ -25,11 +24,109 @@ class PatternDataladDataGrabber(DataladDataGrabber, PatternDataGrabber):
25
24
  types : list of str
26
25
  The types of data to be grabbed.
27
26
  patterns : dict
28
- Patterns for each type of data as a dictionary. The keys are the types
29
- and the values are the patterns. Each occurrence of the string
30
- ``{subject}`` in the pattern will be replaced by the indexed element.
31
- **kwargs
32
- Keyword arguments passed to superclass.
27
+ Data type patterns as a dictionary. It has the following schema:
28
+
29
+ * ``"T1w"`` :
30
+
31
+ .. code-block:: none
32
+
33
+ {
34
+ "mandatory": ["pattern", "space"],
35
+ "optional": []
36
+ }
37
+
38
+ * ``"T2w"`` :
39
+
40
+ .. code-block:: none
41
+
42
+ {
43
+ "mandatory": ["pattern", "space"],
44
+ "optional": []
45
+ }
46
+
47
+ * ``"BOLD"`` :
48
+
49
+ .. code-block:: none
50
+
51
+ {
52
+ "mandatory": ["pattern", "space"],
53
+ "optional": ["mask_item"]
54
+ }
55
+
56
+ * ``"Warp"`` :
57
+
58
+ .. code-block:: none
59
+
60
+ {
61
+ "mandatory": ["pattern", "src", "dst"],
62
+ "optional": []
63
+ }
64
+
65
+ * ``"BOLD_confounds"`` :
66
+
67
+ .. code-block:: none
68
+
69
+ {
70
+ "mandatory": ["pattern", "format"],
71
+ "optional": []
72
+ }
73
+
74
+ * ``"VBM_GM"`` :
75
+
76
+ .. code-block:: none
77
+
78
+ {
79
+ "mandatory": ["pattern", "space"],
80
+ "optional": []
81
+ }
82
+
83
+ * ``"VBM_WM"`` :
84
+
85
+ .. code-block:: none
86
+
87
+ {
88
+ "mandatory": ["pattern", "space"],
89
+ "optional": []
90
+ }
91
+
92
+ Basically, for each data type, one needs to provide ``mandatory`` keys
93
+ and can choose to also provide ``optional`` keys. The value for each
94
+ key is a string. So, one needs to provide necessary data types as a
95
+ dictionary, for example:
96
+
97
+ .. code-block:: none
98
+
99
+ {
100
+ "BOLD": {
101
+ "pattern": "...",
102
+ "space": "...",
103
+ },
104
+ "T1w": {
105
+ "pattern": "...",
106
+ "space": "...",
107
+ },
108
+ "Warp": {
109
+ "pattern": "...",
110
+ "src": "...",
111
+ "dst": "...",
112
+ }
113
+ }
114
+
115
+ taken from :class:`.HCP1200`.
116
+ replacements : str or list of str
117
+ Replacements in the ``pattern`` key of each data type. The value needs
118
+ to be a list of all possible replacements.
119
+ confounds_format : {"fmriprep", "adhoc"} or None, optional
120
+ The format of the confounds for the dataset (default None).
121
+ datadir : str or pathlib.Path or None, optional
122
+ That directory where the datalad dataset will be cloned. If None,
123
+ the datalad dataset will be cloned into a temporary directory
124
+ (default None).
125
+ rootdir : str or pathlib.Path, optional
126
+ The path within the datalad dataset to the root directory
127
+ (default ".").
128
+ uri : str or None, optional
129
+ URI of the datalad sibling (default None).
33
130
 
34
131
  See Also
35
132
  --------
@@ -42,12 +139,13 @@ class PatternDataladDataGrabber(DataladDataGrabber, PatternDataGrabber):
42
139
 
43
140
  def __init__(
44
141
  self,
45
- types: List[str],
46
- patterns: Dict[str, str],
47
142
  **kwargs,
48
143
  ) -> None:
49
- # Validate patterns
50
- validate_patterns(types=types, patterns=patterns)
144
+ # TODO(synchon): needs to be reworked, DataladDataGrabber needs to be
145
+ # a mixin to avoid multiple inheritance wherever possible.
146
+
147
+ logger.debug("Initializing PatternDataladDataGrabber")
148
+ for key, val in kwargs.items():
149
+ logger.debug(f"\t{key} = {val}")
51
150
 
52
- super().__init__(types=types, patterns=patterns, **kwargs)
53
- self.patterns = patterns
151
+ super().__init__(**kwargs)
@@ -12,12 +12,6 @@ import pytest
12
12
  from junifer.datagrabber import BaseDataGrabber
13
13
 
14
14
 
15
- def test_BaseDataGrabber_abstractness() -> None:
16
- """Test BaseDataGrabber is abstract base class."""
17
- with pytest.raises(TypeError, match=r"abstract"):
18
- BaseDataGrabber(datadir="/tmp", types=["func"]) # type: ignore
19
-
20
-
21
15
  def test_BaseDataGrabber() -> None:
22
16
  """Test BaseDataGrabber."""
23
17
 
@@ -3,6 +3,9 @@
3
3
  # Authors: Federico Raimondo <f.raimondo@fz-juelich.de>
4
4
  # License: AGPL
5
5
 
6
+ from contextlib import nullcontext
7
+ from typing import ContextManager, Dict, List, Union
8
+
6
9
  import pytest
7
10
 
8
11
  from junifer.datagrabber.utils import (
@@ -12,79 +15,204 @@ from junifer.datagrabber.utils import (
12
15
  )
13
16
 
14
17
 
15
- def test_validate_types() -> None:
16
- """Test validation of types."""
17
- with pytest.raises(TypeError, match="must be a list"):
18
- validate_types("wrong") # type: ignore
19
- with pytest.raises(TypeError, match="must be a list of strings"):
20
- validate_types([1]) # type: ignore
21
-
22
- validate_types(["T1w", "BOLD"])
23
-
24
-
25
- def test_validate_replacements() -> None:
26
- """Test validation of replacements."""
27
- with pytest.raises(TypeError, match="must be a list"):
28
- validate_replacements("wrong", "also wrong") # type: ignore
29
- with pytest.raises(TypeError, match="must be a dict"):
30
- validate_replacements(["correct"], "wrong") # type: ignore
31
-
32
- patterns = {
33
- "T1w": "{subject}/anat/{subject}_T1w.nii.gz",
34
- "BOLD": "{subject}/func/{subject}_task-rest_bold.nii.gz",
35
- }
36
-
37
- with pytest.raises(TypeError, match="must be a list of strings"):
38
- validate_replacements([1], patterns) # type: ignore
39
-
40
- with pytest.raises(ValueError, match="is not part of"):
41
- validate_replacements(["session"], patterns)
42
-
43
- wrong_patterns = {
44
- "T1w": "{subject}/anat/_T1w.nii.gz",
45
- "BOLD": "{session}/func/_task-rest_bold.nii.gz",
46
- }
47
-
48
- with pytest.raises(ValueError, match="At least one pattern"):
49
- validate_replacements(["subject", "session"], wrong_patterns)
50
-
51
- validate_replacements(["subject"], patterns)
52
-
53
-
54
- def test_validate_patterns() -> None:
55
- """Test validation of patterns."""
56
- types = ["T1w", "BOLD"]
57
- with pytest.raises(TypeError, match="must be a dict"):
58
- validate_patterns(types, "wrong") # type: ignore
59
-
60
- wrongpatterns = {
61
- "T1w": "{subject}/anat/{subject}_T1w.nii.gz",
62
- }
63
-
64
- with pytest.raises(
65
- ValueError, match="Length of `types` more than that of `patterns`."
66
- ):
67
- validate_patterns(types, wrongpatterns) # type: ignore
68
-
69
- wrongpatterns = {
70
- "T1w": "{subject}/anat/{subject}_T1w.nii.gz",
71
- "T2": "{subject}/anat/{subject}_T2.nii.gz",
72
- }
73
-
74
- with pytest.raises(ValueError, match="contain all"):
75
- validate_patterns(types, wrongpatterns) # type: ignore
76
-
77
- patterns = {
78
- "T1w": "{subject}/anat/{subject}_T1w.nii.gz",
79
- "BOLD": "{subject}/func/{subject}_task-rest_bold.nii.gz",
80
- }
81
-
82
- wrongpatterns = {
83
- "T1w": "{subject}/anat/{subject}*.nii",
84
- "BOLD": "{subject}/func/{subject}_task-rest_bold.nii.gz",
85
- }
86
-
87
- with pytest.raises(ValueError, match="following a replacement"):
88
- validate_patterns(types, wrongpatterns)
89
-
90
- validate_patterns(types, patterns)
18
+ @pytest.mark.parametrize(
19
+ "types, expect",
20
+ [
21
+ ("wrong", pytest.raises(TypeError, match="must be a list")),
22
+ ([1], pytest.raises(TypeError, match="must be a list of strings")),
23
+ (["T1w", "BOLD"], nullcontext()),
24
+ ],
25
+ )
26
+ def test_validate_types(
27
+ types: Union[str, List[str], List[int]],
28
+ expect: ContextManager,
29
+ ) -> None:
30
+ """Test validation of types.
31
+
32
+ Parameters
33
+ ----------
34
+ types : str, list of int or str
35
+ The parametrized data types to validate.
36
+ expect : typing.ContextManager
37
+ The parametrized ContextManager object.
38
+
39
+ """
40
+ with expect:
41
+ validate_types(types) # type: ignore
42
+
43
+
44
+ @pytest.mark.parametrize(
45
+ "replacements, patterns, expect",
46
+ [
47
+ (
48
+ "wrong",
49
+ "also wrong",
50
+ pytest.raises(TypeError, match="must be a list"),
51
+ ),
52
+ (
53
+ [1],
54
+ {
55
+ "T1w": {"pattern": "{subject}/anat/{subject}_T1w.nii.gz"},
56
+ "BOLD": {
57
+ "pattern": "{subject}/func/{subject}_task-rest_bold.nii.gz"
58
+ },
59
+ },
60
+ pytest.raises(TypeError, match="must be a list of strings"),
61
+ ),
62
+ (
63
+ ["session"],
64
+ {
65
+ "T1w": {"pattern": "{subject}/anat/{subject}_T1w.nii.gz"},
66
+ "BOLD": {
67
+ "pattern": "{subject}/func/{subject}_task-rest_bold.nii.gz"
68
+ },
69
+ },
70
+ pytest.raises(ValueError, match="is not part of"),
71
+ ),
72
+ (
73
+ ["subject", "session"],
74
+ {
75
+ "T1w": {"pattern": "{subject}/anat/_T1w.nii.gz"},
76
+ "BOLD": {"pattern": "{session}/func/_task-rest_bold.nii.gz"},
77
+ },
78
+ pytest.raises(ValueError, match="At least one pattern"),
79
+ ),
80
+ (
81
+ ["subject"],
82
+ {
83
+ "T1w": {"pattern": "{subject}/anat/{subject}_T1w.nii.gz"},
84
+ "BOLD": {
85
+ "pattern": "{subject}/func/{subject}_task-rest_bold.nii.gz"
86
+ },
87
+ },
88
+ nullcontext(),
89
+ ),
90
+ ],
91
+ )
92
+ def test_validate_replacements(
93
+ replacements: Union[str, List[str], List[int]],
94
+ patterns: Union[str, Dict[str, Dict[str, str]]],
95
+ expect: ContextManager,
96
+ ) -> None:
97
+ """Test validation of replacements.
98
+
99
+ Parameters
100
+ ----------
101
+ replacements : str, list of str or int
102
+ The parametrized pattern replacements to validate.
103
+ patterns : str, dict
104
+ The parametrized patterns to validate against.
105
+ expect : typing.ContextManager
106
+ The parametrized ContextManager object.
107
+
108
+ """
109
+ with expect:
110
+ validate_replacements(replacements=replacements, patterns=patterns) # type: ignore
111
+
112
+
113
+ @pytest.mark.parametrize(
114
+ "types, patterns, expect",
115
+ [
116
+ (
117
+ ["T1w", "BOLD"],
118
+ "wrong",
119
+ pytest.raises(TypeError, match="must be a dict"),
120
+ ),
121
+ (
122
+ ["T1w", "BOLD"],
123
+ {
124
+ "T1w": {"pattern": "{subject}/anat/{subject}_T1w.nii.gz"},
125
+ },
126
+ pytest.raises(
127
+ ValueError,
128
+ match="Length of `types` more than that of `patterns`.",
129
+ ),
130
+ ),
131
+ (
132
+ ["T1w", "BOLD"],
133
+ {
134
+ "T1w": {"pattern": "{subject}/anat/{subject}_T1w.nii.gz"},
135
+ "T2w": {"pattern": "{subject}/anat/{subject}_T2w.nii.gz"},
136
+ },
137
+ pytest.raises(ValueError, match="contain all"),
138
+ ),
139
+ (
140
+ ["T3w"],
141
+ {
142
+ "T3w": {"pattern": "{subject}/anat/{subject}_T3w.nii.gz"},
143
+ },
144
+ pytest.raises(ValueError, match="Unknown data type"),
145
+ ),
146
+ (
147
+ ["BOLD"],
148
+ {
149
+ "BOLD": {"patterns": "{subject}/func/{subject}_BOLD.nii.gz"},
150
+ },
151
+ pytest.raises(KeyError, match="Mandatory key"),
152
+ ),
153
+ (
154
+ ["BOLD_confounds"],
155
+ {
156
+ "BOLD_confounds": {
157
+ "pattern": "{subject}/func/{subject}_confounds.tsv",
158
+ "format": "fmriprep",
159
+ "space": "MNINLin6Asym",
160
+ },
161
+ },
162
+ pytest.raises(RuntimeError, match="not accepted"),
163
+ ),
164
+ (
165
+ ["T1w"],
166
+ {
167
+ "T1w": {
168
+ "pattern": "{subject}/anat/{subject}*.nii",
169
+ "space": "native",
170
+ },
171
+ },
172
+ pytest.raises(ValueError, match="following a replacement"),
173
+ ),
174
+ (
175
+ ["T1w", "T2w", "BOLD", "BOLD_confounds"],
176
+ {
177
+ "T1w": {
178
+ "pattern": "{subject}/anat/{subject}_T1w.nii.gz",
179
+ "space": "native",
180
+ },
181
+ "T2w": {
182
+ "pattern": "{subject}/anat/{subject}_T2w.nii.gz",
183
+ "space": "native",
184
+ },
185
+ "BOLD": {
186
+ "pattern": (
187
+ "{subject}/func/{subject}_task-rest_bold.nii.gz"
188
+ ),
189
+ "space": "MNI152NLin6Asym",
190
+ },
191
+ "BOLD_confounds": {
192
+ "pattern": "{subject}/func/{subject}_confounds.tsv",
193
+ "format": "fmriprep",
194
+ },
195
+ },
196
+ nullcontext(),
197
+ ),
198
+ ],
199
+ )
200
+ def test_validate_patterns(
201
+ types: List[str],
202
+ patterns: Union[str, Dict[str, Dict[str, str]]],
203
+ expect: ContextManager,
204
+ ) -> None:
205
+ """Test validation of patterns.
206
+
207
+ Parameters
208
+ ----------
209
+ types : list of str
210
+ The parametrized data types.
211
+ patterns : str, dict
212
+ The patterns to validate.
213
+ expect : typing.ContextManager
214
+ The parametrized ContextManager object.
215
+
216
+ """
217
+ with expect:
218
+ validate_patterns(types=types, patterns=patterns) # type: ignore
@@ -26,12 +26,6 @@ _testing_dataset = {
26
26
  }
27
27
 
28
28
 
29
- def test_DataladDataGrabber_abstractness() -> None:
30
- """Test DataladDataGrabber is abstract base class."""
31
- with pytest.raises(TypeError, match=r"abstract"):
32
- DataladDataGrabber() # type: ignore
33
-
34
-
35
29
  @pytest.fixture
36
30
  def concrete_datagrabber() -> Type[DataladDataGrabber]:
37
31
  """Return a concrete datalad-based DataGrabber.