Skip to content

Commit fd8cfc2

Browse files
authored
Merge pull request #319 from juaml/enh/raise-invalid-element-selector
[ENH]: Raise/warn if element is not ran
2 parents 7e12e23 + fd25474 commit fd8cfc2

File tree

11 files changed

+186
-65
lines changed

11 files changed

+186
-65
lines changed

docs/changes/newsfragments/319.enh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Raise error when partial or complete element selectors are invalid when running the pipeline by `Synchon Mandal`_

junifer/api/functions.py

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,13 @@
2020
)
2121
from ..preprocess import BasePreprocessor
2222
from ..storage import BaseFeatureStorage
23-
from ..typing import DataGrabberLike, MarkerLike, PreprocessorLike, StorageLike
23+
from ..typing import (
24+
DataGrabberLike,
25+
Elements,
26+
MarkerLike,
27+
PreprocessorLike,
28+
StorageLike,
29+
)
2430
from ..utils import logger, raise_error, warn_with_log, yaml
2531

2632

@@ -121,7 +127,7 @@ def run(
121127
markers: list[dict],
122128
storage: dict,
123129
preprocessors: Optional[list[dict]] = None,
124-
elements: Optional[list[tuple[str, ...]]] = None,
130+
elements: Optional[Elements] = None,
125131
) -> None:
126132
"""Run the pipeline on the selected element.
127133
@@ -147,14 +153,16 @@ def run(
147153
List of preprocessors to use. Each preprocessor is a dict with at
148154
least a key ``kind`` specifying the preprocessor to use. All other keys
149155
are passed to the preprocessor constructor (default None).
150-
elements : list of tuple or None, optional
156+
elements : list or None, optional
151157
Element(s) to process. Will be used to index the DataGrabber
152158
(default None).
153159
154160
Raises
155161
------
156162
ValueError
157163
If ``workdir.cleanup=False`` when ``len(elements) > 1``.
164+
RuntimeError
165+
If invalid element selectors are found.
158166
159167
"""
160168
# Conditional to handle workdir config
@@ -208,10 +216,22 @@ def run(
208216
# Fit elements
209217
with datagrabber_object:
210218
if elements is not None:
211-
for t_element in datagrabber_object.filter(
212-
elements # type: ignore
213-
):
219+
# Keep track of valid selectors
220+
valid_elements = []
221+
for t_element in datagrabber_object.filter(elements):
222+
valid_elements.append(t_element)
214223
mc.fit(datagrabber_object[t_element])
224+
# Compute invalid selectors
225+
invalid_elements = set(elements) - set(valid_elements)
226+
# Report if invalid selectors are found
227+
if invalid_elements:
228+
raise_error(
229+
msg=(
230+
"The following element selectors are invalid:\n"
231+
f"{invalid_elements}"
232+
),
233+
klass=RuntimeError,
234+
)
215235
else:
216236
for t_element in datagrabber_object:
217237
mc.fit(datagrabber_object[t_element])
@@ -243,7 +263,7 @@ def queue(
243263
kind: str,
244264
jobname: str = "junifer_job",
245265
overwrite: bool = False,
246-
elements: Optional[list[tuple[str, ...]]] = None,
266+
elements: Optional[Elements] = None,
247267
**kwargs: Union[str, int, bool, dict, tuple, list],
248268
) -> None:
249269
"""Queue a job to be executed later.
@@ -258,7 +278,7 @@ def queue(
258278
The name of the job (default "junifer_job").
259279
overwrite : bool, optional
260280
Whether to overwrite if job directory already exists (default False).
261-
elements : list of tuple or None, optional
281+
elements : list or None, optional
262282
Element(s) to process. Will be used to index the DataGrabber
263283
(default None).
264284
**kwargs : dict
@@ -341,7 +361,7 @@ def queue(
341361
elements = dg.get_elements()
342362
# Listify elements
343363
if not isinstance(elements, list):
344-
elements: list[Union[str, tuple]] = [elements]
364+
elements: Elements = [elements]
345365

346366
# Check job queueing system
347367
adapter = None
@@ -406,7 +426,7 @@ def reset(config: dict) -> None:
406426

407427
def list_elements(
408428
datagrabber: dict,
409-
elements: Optional[list[tuple[str, ...]]] = None,
429+
elements: Optional[Elements] = None,
410430
) -> str:
411431
"""List elements of the datagrabber filtered using `elements`.
412432
@@ -416,7 +436,7 @@ def list_elements(
416436
DataGrabber to index. Must have a key ``kind`` with the kind of
417437
DataGrabber to use. All other keys are passed to the DataGrabber
418438
constructor.
419-
elements : list of tuple or None, optional
439+
elements : list or None, optional
420440
Element(s) to filter using. Will be used to index the DataGrabber
421441
(default None).
422442

junifer/api/queue_context/gnu_parallel_local_adapter.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66
import shutil
77
import textwrap
88
from pathlib import Path
9-
from typing import Optional, Union
9+
from typing import Optional
1010

11+
from ...typing import Elements
1112
from ...utils import logger, make_executable, raise_error, run_ext_cmd
1213
from .queue_context_adapter import QueueContextAdapter
1314

@@ -63,7 +64,7 @@ def __init__(
6364
job_name: str,
6465
job_dir: Path,
6566
yaml_config_path: Path,
66-
elements: list[Union[str, tuple]],
67+
elements: Elements,
6768
pre_run: Optional[str] = None,
6869
pre_collect: Optional[str] = None,
6970
env: Optional[dict[str, str]] = None,

junifer/api/queue_context/htcondor_adapter.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66
import shutil
77
import textwrap
88
from pathlib import Path
9-
from typing import Optional, Union
9+
from typing import Optional
1010

11+
from ...typing import Elements
1112
from ...utils import logger, make_executable, raise_error, run_ext_cmd
1213
from .queue_context_adapter import QueueContextAdapter
1314

@@ -81,7 +82,7 @@ def __init__(
8182
job_name: str,
8283
job_dir: Path,
8384
yaml_config_path: Path,
84-
elements: list[Union[str, tuple]],
85+
elements: Elements,
8586
pre_run: Optional[str] = None,
8687
pre_collect: Optional[str] = None,
8788
env: Optional[dict[str, str]] = None,

junifer/api/tests/test_functions.py

Lines changed: 115 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,19 @@
66
# License: AGPL
77

88
import logging
9+
from contextlib import AbstractContextManager, nullcontext
910
from pathlib import Path
10-
from typing import Optional, Union
11+
from typing import Any, Optional, Union
1112

1213
import pytest
14+
from nibabel.filebasedimages import ImageFileError
1315
from ruamel.yaml import YAML
1416

1517
import junifer.testing.registry # noqa: F401
1618
from junifer.api import collect, list_elements, queue, reset, run
1719
from junifer.datagrabber.base import BaseDataGrabber
1820
from junifer.pipeline import PipelineComponentRegistry
21+
from junifer.typing import Elements
1922

2023

2124
# Configure YAML class
@@ -25,12 +28,37 @@
2528
yaml.indent(mapping=2, sequence=4, offset=2)
2629

2730

31+
# Kept for parametrizing
32+
_datagrabber = {
33+
"kind": "PartlyCloudyTestingDataGrabber",
34+
}
35+
_bids_ses_datagrabber = {
36+
"kind": "PatternDataladDataGrabber",
37+
"uri": "https://gin.g-node.org/juaml/datalad-example-bids-ses",
38+
"types": ["T1w", "BOLD"],
39+
"patterns": {
40+
"T1w": {
41+
"pattern": (
42+
"{subject}/{session}/anat/{subject}_{session}_T1w.nii.gz"
43+
),
44+
"space": "MNI152NLin6Asym",
45+
},
46+
"BOLD": {
47+
"pattern": (
48+
"{subject}/{session}/func/{subject}_{session}_task-rest_bold.nii.gz"
49+
),
50+
"space": "MNI152NLin6Asym",
51+
},
52+
},
53+
"replacements": ["subject", "session"],
54+
"rootdir": "example_bids_ses",
55+
}
56+
57+
2858
@pytest.fixture
2959
def datagrabber() -> dict[str, str]:
3060
"""Return a datagrabber as a dictionary."""
31-
return {
32-
"kind": "PartlyCloudyTestingDataGrabber",
33-
}
61+
return _datagrabber.copy()
3462

3563

3664
@pytest.fixture
@@ -60,11 +88,48 @@ def storage() -> dict[str, str]:
6088
}
6189

6290

91+
@pytest.mark.parametrize(
92+
"datagrabber, element, expect",
93+
[
94+
(
95+
_datagrabber,
96+
[("sub-01",)],
97+
pytest.raises(RuntimeError, match="element selectors are invalid"),
98+
),
99+
(
100+
_datagrabber,
101+
["sub-01"],
102+
nullcontext(),
103+
),
104+
(
105+
_bids_ses_datagrabber,
106+
["sub-01"],
107+
pytest.raises(ImageFileError, match="is not a gzip file"),
108+
),
109+
(
110+
_bids_ses_datagrabber,
111+
[("sub-01", "ses-01")],
112+
pytest.raises(ImageFileError, match="is not a gzip file"),
113+
),
114+
(
115+
_bids_ses_datagrabber,
116+
[("sub-01", "ses-100")],
117+
pytest.raises(RuntimeError, match="element selectors are invalid"),
118+
),
119+
(
120+
_bids_ses_datagrabber,
121+
[("sub-100", "ses-01")],
122+
pytest.raises(RuntimeError, match="element selectors are invalid"),
123+
),
124+
],
125+
)
63126
def test_run_single_element(
64127
tmp_path: Path,
65-
datagrabber: dict[str, str],
128+
datagrabber: dict[str, Any],
66129
markers: list[dict[str, str]],
67130
storage: dict[str, str],
131+
element: Elements,
132+
expect: AbstractContextManager,
68133
) -> None:
69134
"""Test run function with single element.
70135
@@ -78,21 +143,26 @@ def test_run_single_element(
78143
Testing markers as list of dictionary.
79144
storage : dict
80145
Testing storage as dictionary.
146+
element : list of str or tuple
147+
The parametrized element.
148+
expect : typing.ContextManager
149+
The parametrized ContextManager object.
81150
82151
"""
83152
# Set storage
84153
storage["uri"] = str((tmp_path / "out.sqlite").resolve())
85154
# Run operations
86-
run(
87-
workdir=tmp_path,
88-
datagrabber=datagrabber,
89-
markers=markers,
90-
storage=storage,
91-
elements=[("sub-01",)],
92-
)
93-
# Check files
94-
files = list(tmp_path.glob("*.sqlite"))
95-
assert len(files) == 1
155+
with expect:
156+
run(
157+
workdir=tmp_path,
158+
datagrabber=datagrabber,
159+
markers=markers,
160+
storage=storage,
161+
elements=element,
162+
)
163+
# Check files
164+
files = list(tmp_path.glob("*.sqlite"))
165+
assert len(files) == 1
96166

97167

98168
def test_run_single_element_with_preprocessing(
@@ -128,18 +198,30 @@ def test_run_single_element_with_preprocessing(
128198
"kind": "fMRIPrepConfoundRemover",
129199
}
130200
],
131-
elements=[("sub-01",)],
201+
elements=["sub-01"],
132202
)
133203
# Check files
134204
files = list(tmp_path.glob("*.sqlite"))
135205
assert len(files) == 1
136206

137207

208+
@pytest.mark.parametrize(
209+
"element, expect",
210+
[
211+
(
212+
[("sub-01",), ("sub-03",)],
213+
pytest.raises(RuntimeError, match="element selectors are invalid"),
214+
),
215+
(["sub-01", "sub-03"], nullcontext()),
216+
],
217+
)
138218
def test_run_multi_element_multi_output(
139219
tmp_path: Path,
140220
datagrabber: dict[str, str],
141221
markers: list[dict[str, str]],
142222
storage: dict[str, str],
223+
element: Elements,
224+
expect: AbstractContextManager,
143225
) -> None:
144226
"""Test run function with multi element and multi output.
145227
@@ -153,22 +235,27 @@ def test_run_multi_element_multi_output(
153235
Testing markers as list of dictionary.
154236
storage : dict
155237
Testing storage as dictionary.
238+
element : list of str or tuple
239+
The parametrized element.
240+
expect : typing.ContextManager
241+
The parametrized ContextManager object.
156242
157243
"""
158244
# Set storage
159245
storage["uri"] = str((tmp_path / "out.sqlite").resolve())
160246
storage["single_output"] = False # type: ignore
161247
# Run operations
162-
run(
163-
workdir=tmp_path,
164-
datagrabber=datagrabber,
165-
markers=markers,
166-
storage=storage,
167-
elements=[("sub-01",), ("sub-03",)],
168-
)
169-
# Check files
170-
files = list(tmp_path.glob("*.sqlite"))
171-
assert len(files) == 2
248+
with expect:
249+
run(
250+
workdir=tmp_path,
251+
datagrabber=datagrabber,
252+
markers=markers,
253+
storage=storage,
254+
elements=element,
255+
)
256+
# Check files
257+
files = list(tmp_path.glob("*.sqlite"))
258+
assert len(files) == 2
172259

173260

174261
def test_run_multi_element_single_output(
@@ -200,7 +287,7 @@ def test_run_multi_element_single_output(
200287
datagrabber=datagrabber,
201288
markers=markers,
202289
storage=storage,
203-
elements=[("sub-01",), ("sub-03",)],
290+
elements=["sub-01", "sub-03"],
204291
)
205292
# Check files
206293
files = list(tmp_path.glob("*.sqlite"))
@@ -569,7 +656,7 @@ def test_reset_run(
569656
datagrabber=datagrabber,
570657
markers=markers,
571658
storage=storage,
572-
elements=[("sub-01",)],
659+
elements=["sub-01"],
573660
)
574661
# Reset operation
575662
reset(config={"storage": storage})

0 commit comments

Comments
 (0)