junifer 0.0.5.dev219__py3-none-any.whl → 0.0.5.dev242__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.
@@ -25,28 +25,26 @@ def test_MultipleDataGrabber() -> None:
25
25
  repo_uri = _testing_dataset["example_bids_ses"]["uri"]
26
26
  rootdir = "example_bids_ses"
27
27
  replacements = ["subject", "session"]
28
- pattern1 = {
29
- "T1w": {
30
- "pattern": (
31
- "{subject}/{session}/anat/{subject}_{session}_T1w.nii.gz"
32
- ),
33
- "space": "native",
34
- },
35
- }
36
- pattern2 = {
37
- "BOLD": {
38
- "pattern": (
39
- "{subject}/{session}/func/"
40
- "{subject}_{session}_task-rest_bold.nii.gz"
41
- ),
42
- "space": "MNI152NLin6Asym",
43
- },
44
- }
28
+
45
29
  dg1 = PatternDataladDataGrabber(
46
30
  rootdir=rootdir,
47
31
  uri=repo_uri,
48
32
  types=["T1w"],
49
- patterns=pattern1,
33
+ patterns={
34
+ "T1w": {
35
+ "pattern": (
36
+ "{subject}/{session}/anat/{subject}_{session}_T1w.nii.gz"
37
+ ),
38
+ "space": "native",
39
+ "mask": {
40
+ "pattern": (
41
+ "{subject}/{session}/anat/{subject}_{session}_"
42
+ "brain_mask.nii.gz"
43
+ ),
44
+ "space": "native",
45
+ },
46
+ },
47
+ },
50
48
  replacements=replacements,
51
49
  )
52
50
 
@@ -54,7 +52,22 @@ def test_MultipleDataGrabber() -> None:
54
52
  rootdir=rootdir,
55
53
  uri=repo_uri,
56
54
  types=["BOLD"],
57
- patterns=pattern2,
55
+ patterns={
56
+ "BOLD": {
57
+ "pattern": (
58
+ "{subject}/{session}/func/"
59
+ "{subject}_{session}_task-rest_bold.nii.gz"
60
+ ),
61
+ "space": "MNI152NLin6Asym",
62
+ "mask": {
63
+ "pattern": (
64
+ "{subject}/{session}/func/"
65
+ "{subject}_{session}_task-rest_brain_mask.nii.gz"
66
+ ),
67
+ "space": "MNI152NLin6Asym",
68
+ },
69
+ },
70
+ },
58
71
  replacements=replacements,
59
72
  )
60
73
 
@@ -73,14 +86,17 @@ def test_MultipleDataGrabber() -> None:
73
86
  with dg:
74
87
  subs = list(dg)
75
88
  assert set(subs) == set(expected_subs)
76
-
89
+ # Check data type
77
90
  elem = dg[("sub-01", "ses-01")]
91
+ # Check data types
78
92
  assert "T1w" in elem
79
93
  assert "BOLD" in elem
94
+ # Check meta
80
95
  assert "meta" in elem["BOLD"]
81
96
  meta = elem["BOLD"]["meta"]["datagrabber"]
82
97
  assert "class" in meta
83
98
  assert meta["class"] == "MultipleDataGrabber"
99
+ # Check datagrabbers
84
100
  assert "datagrabbers" in meta
85
101
  assert len(meta["datagrabbers"]) == 2
86
102
  assert meta["datagrabbers"][0]["class"] == "PatternDataladDataGrabber"
@@ -89,40 +105,37 @@ def test_MultipleDataGrabber() -> None:
89
105
 
90
106
  def test_MultipleDataGrabber_no_intersection() -> None:
91
107
  """Test MultipleDataGrabber without intersection (0 elements)."""
92
- repo_uri1 = _testing_dataset["example_bids"]["uri"]
93
- repo_uri2 = _testing_dataset["example_bids_ses"]["uri"]
94
108
  rootdir = "example_bids_ses"
95
109
  replacements = ["subject", "session"]
96
- pattern1 = {
97
- "T1w": {
98
- "pattern": (
99
- "{subject}/{session}/anat/{subject}_{session}_T1w.nii.gz"
100
- ),
101
- "space": "native",
102
- },
103
- }
104
- pattern2 = {
105
- "BOLD": {
106
- "pattern": (
107
- "{subject}/{session}/func/"
108
- "{subject}_{session}_task-rest_bold.nii.gz"
109
- ),
110
- "space": "MNI152NLin6Asym",
111
- },
112
- }
110
+
113
111
  dg1 = PatternDataladDataGrabber(
114
112
  rootdir=rootdir,
115
- uri=repo_uri1,
113
+ uri=_testing_dataset["example_bids"]["uri"],
116
114
  types=["T1w"],
117
- patterns=pattern1,
115
+ patterns={
116
+ "T1w": {
117
+ "pattern": (
118
+ "{subject}/{session}/anat/{subject}_{session}_T1w.nii.gz"
119
+ ),
120
+ "space": "native",
121
+ },
122
+ },
118
123
  replacements=replacements,
119
124
  )
120
125
 
121
126
  dg2 = PatternDataladDataGrabber(
122
127
  rootdir=rootdir,
123
- uri=repo_uri2,
128
+ uri=_testing_dataset["example_bids_ses"]["uri"],
124
129
  types=["BOLD"],
125
- patterns=pattern2,
130
+ patterns={
131
+ "BOLD": {
132
+ "pattern": (
133
+ "{subject}/{session}/func/"
134
+ "{subject}_{session}_task-rest_bold.nii.gz"
135
+ ),
136
+ "space": "MNI152NLin6Asym",
137
+ },
138
+ },
126
139
  replacements=replacements,
127
140
  )
128
141
 
@@ -135,23 +148,19 @@ def test_MultipleDataGrabber_no_intersection() -> None:
135
148
 
136
149
  def test_MultipleDataGrabber_get_item() -> None:
137
150
  """Test MultipleDataGrabber get_item() error."""
138
- repo_uri1 = _testing_dataset["example_bids"]["uri"]
139
- rootdir = "example_bids_ses"
140
- replacements = ["subject", "session"]
141
- pattern1 = {
142
- "T1w": {
143
- "pattern": (
144
- "{subject}/{session}/anat/{subject}_{session}_T1w.nii.gz"
145
- ),
146
- "space": "native",
147
- },
148
- }
149
151
  dg1 = PatternDataladDataGrabber(
150
- rootdir=rootdir,
151
- uri=repo_uri1,
152
+ rootdir="example_bids_ses",
153
+ uri=_testing_dataset["example_bids"]["uri"],
152
154
  types=["T1w"],
153
- patterns=pattern1,
154
- replacements=replacements,
155
+ patterns={
156
+ "T1w": {
157
+ "pattern": (
158
+ "{subject}/{session}/anat/{subject}_{session}_T1w.nii.gz"
159
+ ),
160
+ "space": "native",
161
+ },
162
+ },
163
+ replacements=["subject", "session"],
155
164
  )
156
165
 
157
166
  dg = MultipleDataGrabber([dg1])
@@ -161,43 +170,111 @@ def test_MultipleDataGrabber_get_item() -> None:
161
170
 
162
171
  def test_MultipleDataGrabber_validation() -> None:
163
172
  """Test MultipleDataGrabber init validation."""
164
- repo_uri1 = _testing_dataset["example_bids"]["uri"]
165
- repo_uri2 = _testing_dataset["example_bids_ses"]["uri"]
166
173
  rootdir = "example_bids_ses"
167
- replacement1 = ["subject", "session"]
168
- replacement2 = ["subject"]
169
- pattern1 = {
170
- "T1w": {
171
- "pattern": (
172
- "{subject}/{session}/anat/{subject}_{session}_T1w.nii.gz"
173
- ),
174
- "space": "native",
175
- },
176
- }
177
- pattern2 = {
178
- "BOLD": {
179
- "pattern": "{subject}/func/{subject}_task-rest_bold.nii.gz",
180
- "space": "MNI152NLin6Asym",
181
- },
182
- }
174
+
183
175
  dg1 = PatternDataladDataGrabber(
184
176
  rootdir=rootdir,
185
- uri=repo_uri1,
177
+ uri=_testing_dataset["example_bids"]["uri"],
186
178
  types=["T1w"],
187
- patterns=pattern1,
188
- replacements=replacement1,
179
+ patterns={
180
+ "T1w": {
181
+ "pattern": (
182
+ "{subject}/{session}/anat/{subject}_{session}_T1w.nii.gz"
183
+ ),
184
+ "space": "native",
185
+ },
186
+ },
187
+ replacements=["subject", "session"],
189
188
  )
190
189
 
191
190
  dg2 = PatternDataladDataGrabber(
192
191
  rootdir=rootdir,
193
- uri=repo_uri2,
192
+ uri=_testing_dataset["example_bids_ses"]["uri"],
194
193
  types=["BOLD"],
195
- patterns=pattern2,
196
- replacements=replacement2,
194
+ patterns={
195
+ "BOLD": {
196
+ "pattern": "{subject}/func/{subject}_task-rest_bold.nii.gz",
197
+ "space": "MNI152NLin6Asym",
198
+ },
199
+ },
200
+ replacements=["subject"],
197
201
  )
198
202
 
199
- with pytest.raises(ValueError, match="different element key"):
203
+ with pytest.raises(RuntimeError, match="have different element keys"):
200
204
  MultipleDataGrabber([dg1, dg2])
201
205
 
202
- with pytest.raises(ValueError, match="overlapping types"):
206
+ with pytest.raises(RuntimeError, match="have overlapping mandatory"):
203
207
  MultipleDataGrabber([dg1, dg1])
208
+
209
+
210
+ def test_MultipleDataGrabber_partial_pattern() -> None:
211
+ """Test MultipleDataGrabber partial pattern."""
212
+ repo_uri = _testing_dataset["example_bids_ses"]["uri"]
213
+ rootdir = "example_bids_ses"
214
+ replacements = ["subject", "session"]
215
+
216
+ dg1 = PatternDataladDataGrabber(
217
+ rootdir=rootdir,
218
+ uri=repo_uri,
219
+ types=["BOLD"],
220
+ patterns={
221
+ "BOLD": {
222
+ "pattern": (
223
+ "{subject}/{session}/func/"
224
+ "{subject}_{session}_task-rest_bold.nii.gz"
225
+ ),
226
+ "space": "MNI152NLin6Asym",
227
+ },
228
+ },
229
+ replacements=replacements,
230
+ )
231
+
232
+ dg2 = PatternDataladDataGrabber(
233
+ rootdir=rootdir,
234
+ uri=repo_uri,
235
+ types=["BOLD"],
236
+ patterns={
237
+ "BOLD": {
238
+ "confounds": {
239
+ "pattern": (
240
+ "{subject}/{session}/func/"
241
+ "{subject}_{session}_task-rest_"
242
+ "confounds_regressors.tsv"
243
+ ),
244
+ "format": "fmriprep",
245
+ },
246
+ },
247
+ },
248
+ replacements=["subject", "session"],
249
+ partial_pattern_ok=True,
250
+ )
251
+
252
+ dg = MultipleDataGrabber([dg1, dg2])
253
+
254
+ types = dg.get_types()
255
+ assert "BOLD" in types
256
+
257
+ expected_subs = [
258
+ (f"sub-{i:02d}", f"ses-{j:02d}")
259
+ for j in range(1, 3)
260
+ for i in range(1, 10)
261
+ ]
262
+
263
+ with dg:
264
+ subs = list(dg)
265
+ assert set(subs) == set(expected_subs)
266
+ # Fetch element
267
+ elem = dg[("sub-01", "ses-01")]
268
+ # Check data type and nested data type
269
+ assert "BOLD" in elem
270
+ assert "confounds" in elem["BOLD"]
271
+ # Check meta
272
+ assert "meta" in elem["BOLD"]
273
+ meta = elem["BOLD"]["meta"]["datagrabber"]
274
+ assert "class" in meta
275
+ assert meta["class"] == "MultipleDataGrabber"
276
+ # Check datagrabbers
277
+ assert "datagrabbers" in meta
278
+ assert len(meta["datagrabbers"]) == 2
279
+ assert meta["datagrabbers"][0]["class"] == "PatternDataladDataGrabber"
280
+ assert meta["datagrabbers"][1]["class"] == "PatternDataladDataGrabber"
@@ -1,6 +1,7 @@
1
- """Provide tests for utils."""
1
+ """Provide tests for PatternValidationMixin."""
2
2
 
3
3
  # Authors: Federico Raimondo <f.raimondo@fz-juelich.de>
4
+ # Synchon Mandal <s.mandal@fz-juelich.de>
4
5
  # License: AGPL
5
6
 
6
7
  from contextlib import nullcontext
@@ -8,136 +9,57 @@ from typing import ContextManager, Dict, List, Union
8
9
 
9
10
  import pytest
10
11
 
11
- from junifer.datagrabber.utils import (
12
- validate_patterns,
13
- validate_replacements,
14
- validate_types,
15
- )
12
+ from junifer.datagrabber.pattern_validation_mixin import PatternValidationMixin
16
13
 
17
14
 
18
15
  @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",
16
+ "types, replacements, patterns, expect",
46
17
  [
47
18
  (
48
19
  "wrong",
49
- "also wrong",
50
- pytest.raises(TypeError, match="must be a list"),
20
+ [],
21
+ {},
22
+ pytest.raises(TypeError, match="`types` must be a list"),
51
23
  ),
52
24
  (
53
25
  [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"),
26
+ [],
27
+ {},
28
+ pytest.raises(
29
+ TypeError, match="`types` must be a list of strings"
30
+ ),
79
31
  ),
80
32
  (
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"],
33
+ ["BOLD"],
34
+ [],
118
35
  "wrong",
119
- pytest.raises(TypeError, match="must be a dict"),
36
+ pytest.raises(TypeError, match="`patterns` must be a dict"),
120
37
  ),
121
38
  (
122
39
  ["T1w", "BOLD"],
40
+ "",
123
41
  {
124
42
  "T1w": {"pattern": "{subject}/anat/{subject}_T1w.nii.gz"},
125
43
  },
126
44
  pytest.raises(
127
45
  ValueError,
128
- match="Length of `types` more than that of `patterns`.",
46
+ match="Length of `types` more than that of `patterns`",
129
47
  ),
130
48
  ),
131
49
  (
132
50
  ["T1w", "BOLD"],
51
+ "",
133
52
  {
134
53
  "T1w": {"pattern": "{subject}/anat/{subject}_T1w.nii.gz"},
135
54
  "T2w": {"pattern": "{subject}/anat/{subject}_T2w.nii.gz"},
136
55
  },
137
- pytest.raises(ValueError, match="contain all"),
56
+ pytest.raises(
57
+ ValueError, match="`patterns` must contain all `types`"
58
+ ),
138
59
  ),
139
60
  (
140
61
  ["T3w"],
62
+ "",
141
63
  {
142
64
  "T3w": {"pattern": "{subject}/anat/{subject}_T3w.nii.gz"},
143
65
  },
@@ -145,6 +67,7 @@ def test_validate_replacements(
145
67
  ),
146
68
  (
147
69
  ["BOLD"],
70
+ "",
148
71
  {
149
72
  "BOLD": {"patterns": "{subject}/func/{subject}_BOLD.nii.gz"},
150
73
  },
@@ -152,6 +75,7 @@ def test_validate_replacements(
152
75
  ),
153
76
  (
154
77
  ["BOLD"],
78
+ "",
155
79
  {
156
80
  "BOLD": {
157
81
  "pattern": (
@@ -169,6 +93,7 @@ def test_validate_replacements(
169
93
  ),
170
94
  (
171
95
  ["T1w"],
96
+ "",
172
97
  {
173
98
  "T1w": {
174
99
  "pattern": "{subject}/anat/{subject}*.nii",
@@ -177,8 +102,65 @@ def test_validate_replacements(
177
102
  },
178
103
  pytest.raises(ValueError, match="following a replacement"),
179
104
  ),
105
+ (
106
+ ["T1w"],
107
+ "wrong",
108
+ {
109
+ "T1w": {
110
+ "pattern": "{subject}/anat/{subject}_T1w.nii",
111
+ "space": "native",
112
+ },
113
+ },
114
+ pytest.raises(TypeError, match="`replacements` must be a list"),
115
+ ),
116
+ (
117
+ ["T1w"],
118
+ [1],
119
+ {
120
+ "T1w": {
121
+ "pattern": "{subject}/anat/{subject}_T1w.nii",
122
+ "space": "native",
123
+ },
124
+ },
125
+ pytest.raises(
126
+ TypeError, match="`replacements` must be a list of strings"
127
+ ),
128
+ ),
129
+ (
130
+ ["T1w", "BOLD"],
131
+ ["subject", "session"],
132
+ {
133
+ "T1w": {
134
+ "pattern": "{subject}/anat/{subject}_T1w.nii.gz",
135
+ "space": "native",
136
+ },
137
+ "BOLD": {
138
+ "pattern": (
139
+ "{subject}/func/{subject}_task-rest_bold.nii.gz"
140
+ ),
141
+ "space": "MNI152NLin6Asym",
142
+ },
143
+ },
144
+ pytest.raises(ValueError, match="is not part of any pattern"),
145
+ ),
146
+ (
147
+ ["BOLD"],
148
+ ["subject", "session"],
149
+ {
150
+ "T1w": {
151
+ "pattern": "{subject}/anat/_T1w.nii.gz",
152
+ "space": "native",
153
+ },
154
+ "BOLD": {
155
+ "pattern": "{session}/func/_task-rest_bold.nii.gz",
156
+ "space": "MNI152NLin6Asym",
157
+ },
158
+ },
159
+ pytest.raises(ValueError, match="At least one pattern"),
160
+ ),
180
161
  (
181
162
  ["T1w", "T2w", "BOLD"],
163
+ ["subject"],
182
164
  {
183
165
  "T1w": {
184
166
  "pattern": "{subject}/anat/{subject}_T1w.nii.gz",
@@ -190,7 +172,7 @@ def test_validate_replacements(
190
172
  },
191
173
  "BOLD": {
192
174
  "pattern": (
193
- "{subject}/func/{subject}_task-rest_bold.nii.gz"
175
+ "{subject}/func/{session}/{subject}_task-rest_bold.nii.gz"
194
176
  ),
195
177
  "space": "MNI152NLin6Asym",
196
178
  "confounds": {
@@ -203,22 +185,65 @@ def test_validate_replacements(
203
185
  ),
204
186
  ],
205
187
  )
206
- def test_validate_patterns(
207
- types: List[str],
188
+ def test_PatternValidationMixin(
189
+ types: Union[str, List[str], List[int]],
190
+ replacements: Union[str, List[str], List[int]],
208
191
  patterns: Union[str, Dict[str, Dict[str, str]]],
209
192
  expect: ContextManager,
210
193
  ) -> None:
211
- """Test validation of patterns.
194
+ """Test validation.
212
195
 
213
196
  Parameters
214
197
  ----------
215
- types : list of str
216
- The parametrized data types.
198
+ types : str, list of int or str
199
+ The parametrized data types to validate.
200
+ replacements : str, list of str or int
201
+ The parametrized pattern replacements to validate.
217
202
  patterns : str, dict
218
- The patterns to validate.
203
+ The parametrized patterns to validate against.
219
204
  expect : typing.ContextManager
220
205
  The parametrized ContextManager object.
221
206
 
222
207
  """
208
+
209
+ class MockDataGrabber(PatternValidationMixin):
210
+ def __init__(
211
+ self,
212
+ types,
213
+ replacements,
214
+ patterns,
215
+ ) -> None:
216
+ self.types = types
217
+ self.replacements = replacements
218
+ self.patterns = patterns
219
+
220
+ def validate(self) -> None:
221
+ self.validate_patterns(
222
+ types=self.types,
223
+ replacements=self.replacements,
224
+ patterns=self.patterns,
225
+ )
226
+
227
+ dg = MockDataGrabber(types, replacements, patterns)
223
228
  with expect:
224
- validate_patterns(types=types, patterns=patterns) # type: ignore
229
+ dg.validate()
230
+
231
+
232
+ # This test is kept separate as bool doesn't support context manager protocol,
233
+ # used in the earlier test
234
+ def test_PatternValidationMixin_partial_pattern_check() -> None:
235
+ """Test validation for partial patterns."""
236
+ with pytest.warns(RuntimeWarning, match="might not work as expected"):
237
+ PatternValidationMixin().validate_patterns(
238
+ types=["BOLD"],
239
+ replacements=["subject"],
240
+ patterns={
241
+ "BOLD": {
242
+ "mask": {
243
+ "pattern": "{subject}/func/{subject}_BOLD.nii.gz",
244
+ "space": "MNI152NLin6Asym",
245
+ },
246
+ },
247
+ }, # type: ignore
248
+ partial_pattern_ok=True,
249
+ )
junifer/utils/__init__.py CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  from .fs import make_executable
8
8
  from .logging import configure_logging, logger, raise_error, warn_with_log
9
- from .helpers import run_ext_cmd
9
+ from .helpers import run_ext_cmd, deep_update
10
10
 
11
11
 
12
12
  __all__ = [
@@ -16,4 +16,5 @@ __all__ = [
16
16
  "raise_error",
17
17
  "warn_with_log",
18
18
  "run_ext_cmd",
19
+ "deep_update",
19
20
  ]