junifer 0.0.7.dev332__py3-none-any.whl → 0.0.8__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 (293) hide show
  1. junifer/_version.py +9 -19
  2. junifer/api/__init__.pyi +17 -2
  3. junifer/api/functions.py +222 -39
  4. junifer/api/queue_context/__init__.pyi +11 -3
  5. junifer/api/queue_context/gnu_parallel_local_adapter.py +81 -120
  6. junifer/api/queue_context/htcondor_adapter.py +118 -181
  7. junifer/api/queue_context/queue_context_adapter.py +51 -7
  8. junifer/api/queue_context/tests/test_gnu_parallel_local_adapter.py +10 -35
  9. junifer/api/queue_context/tests/test_htcondor_adapter.py +13 -50
  10. junifer/api/res/fsl/run_fsl_docker.sh +4 -0
  11. junifer/api/tests/test_functions.py +863 -36
  12. junifer/cli/__init__.py +1 -7
  13. junifer/cli/cli.py +41 -22
  14. junifer/cli/parser.py +23 -147
  15. junifer/cli/tests/data/gmd_mean.yaml +6 -6
  16. junifer/cli/tests/data/gmd_mean_htcondor.yaml +6 -5
  17. junifer/cli/tests/data/partly_cloudy_agg_mean_tian.yml +1 -0
  18. junifer/cli/tests/test_cli.py +120 -4
  19. junifer/cli/tests/test_cli_utils.py +23 -6
  20. junifer/cli/utils.py +14 -9
  21. junifer/configs/__init__.py +5 -0
  22. junifer/configs/__init__.pyi +0 -0
  23. junifer/configs/juseless/datagrabbers/__init__.pyi +4 -2
  24. junifer/configs/juseless/datagrabbers/aomic_id1000_vbm.py +18 -24
  25. junifer/configs/juseless/datagrabbers/camcan_vbm.py +20 -27
  26. junifer/configs/juseless/datagrabbers/ixi_vbm.py +44 -52
  27. junifer/configs/juseless/datagrabbers/tests/test_ixi_vbm.py +0 -7
  28. junifer/configs/juseless/datagrabbers/tests/test_ucla.py +2 -19
  29. junifer/configs/juseless/datagrabbers/ucla.py +111 -104
  30. junifer/configs/juseless/datagrabbers/ukb_vbm.py +19 -25
  31. junifer/configs/py.typed +0 -0
  32. junifer/conftest.py +63 -24
  33. junifer/data/_dispatch.py +9 -19
  34. junifer/data/coordinates/_ants_coordinates_warper.py +4 -1
  35. junifer/data/coordinates/_coordinates.py +51 -6
  36. junifer/data/coordinates/_fsl_coordinates_warper.py +4 -1
  37. junifer/data/coordinates/tests/test_coordinates.py +15 -3
  38. junifer/data/coordinates/tests/test_coordinates_names.py +37 -0
  39. junifer/data/maps/_ants_maps_warper.py +18 -12
  40. junifer/data/maps/_fsl_maps_warper.py +7 -3
  41. junifer/data/maps/_maps.py +12 -8
  42. junifer/data/maps/tests/test_maps.py +13 -21
  43. junifer/data/masks/_ants_mask_warper.py +18 -12
  44. junifer/data/masks/_fsl_mask_warper.py +7 -3
  45. junifer/data/masks/_masks.py +15 -12
  46. junifer/data/masks/tests/test_masks.py +20 -17
  47. junifer/data/parcellations/_ants_parcellation_warper.py +18 -12
  48. junifer/data/parcellations/_fsl_parcellation_warper.py +7 -5
  49. junifer/data/parcellations/_parcellations.py +125 -91
  50. junifer/data/parcellations/tests/test_parcellations.py +122 -1
  51. junifer/data/template_spaces.py +117 -50
  52. junifer/data/tests/test_template_spaces.py +10 -7
  53. junifer/data/utils.py +10 -8
  54. junifer/datagrabber/__init__.pyi +38 -5
  55. junifer/datagrabber/aomic/__init__.pyi +8 -1
  56. junifer/datagrabber/aomic/_types.py +22 -0
  57. junifer/datagrabber/aomic/id1000.py +193 -183
  58. junifer/datagrabber/aomic/piop1.py +215 -259
  59. junifer/datagrabber/aomic/piop2.py +211 -244
  60. junifer/datagrabber/aomic/tests/test_id1000.py +70 -53
  61. junifer/datagrabber/aomic/tests/test_piop1.py +98 -102
  62. junifer/datagrabber/aomic/tests/test_piop2.py +88 -85
  63. junifer/datagrabber/base.py +253 -79
  64. junifer/datagrabber/datalad_base.py +127 -76
  65. junifer/datagrabber/dmcc13_benchmark.py +204 -289
  66. junifer/datagrabber/hcp1200/__init__.pyi +7 -2
  67. junifer/datagrabber/hcp1200/datalad_hcp1200.py +61 -40
  68. junifer/datagrabber/hcp1200/hcp1200.py +151 -164
  69. junifer/datagrabber/hcp1200/tests/test_hcp1200.py +116 -128
  70. junifer/datagrabber/multiple.py +183 -63
  71. junifer/datagrabber/pattern.py +358 -227
  72. junifer/datagrabber/pattern_datalad.py +84 -124
  73. junifer/datagrabber/pattern_validation_mixin.py +22 -50
  74. junifer/datagrabber/tests/test_base.py +17 -4
  75. junifer/datagrabber/tests/test_datalad_base.py +20 -27
  76. junifer/datagrabber/tests/test_dmcc13_benchmark.py +110 -122
  77. junifer/datagrabber/tests/test_multiple.py +293 -31
  78. junifer/datagrabber/tests/test_pattern.py +577 -18
  79. junifer/datagrabber/tests/test_pattern_datalad.py +177 -107
  80. junifer/datagrabber/tests/test_pattern_validation_mixin.py +9 -51
  81. junifer/datareader/default.py +8 -5
  82. junifer/datareader/tests/test_default_reader.py +1 -1
  83. junifer/external/BrainPrint/.codecov.yml +2 -0
  84. junifer/external/BrainPrint/.codespellignore +2 -0
  85. junifer/external/BrainPrint/.github/ISSUE_TEMPLATE/bug-report.md +45 -0
  86. junifer/external/BrainPrint/.github/ISSUE_TEMPLATE/documentation.md +12 -0
  87. junifer/external/BrainPrint/.github/ISSUE_TEMPLATE/feature-request.md +24 -0
  88. junifer/external/BrainPrint/.github/ISSUE_TEMPLATE/questions-help-support.md +28 -0
  89. junifer/external/BrainPrint/.github/dependabot.yml +6 -0
  90. junifer/external/BrainPrint/.github/workflows/build.yml +53 -0
  91. junifer/external/BrainPrint/.github/workflows/code-style.yml +44 -0
  92. junifer/external/BrainPrint/.github/workflows/doc.yml +65 -0
  93. junifer/external/BrainPrint/.github/workflows/publish.yml +31 -0
  94. junifer/external/BrainPrint/.github/workflows/pytest.yml +54 -0
  95. junifer/external/BrainPrint/.gitignore +123 -0
  96. junifer/external/BrainPrint/LICENSE +21 -0
  97. junifer/external/BrainPrint/README.md +108 -0
  98. junifer/external/BrainPrint/brainprint/cli/__init__.py +11 -0
  99. junifer/external/BrainPrint/brainprint/cli/help_text.py +169 -0
  100. junifer/external/BrainPrint/brainprint/cli/parser.py +150 -0
  101. junifer/external/BrainPrint/brainprint/cli/utils.py +14 -0
  102. junifer/external/BrainPrint/brainprint/commands/__init__.py +0 -0
  103. junifer/external/BrainPrint/brainprint/commands/sys_info.py +18 -0
  104. junifer/external/BrainPrint/brainprint/utils/tests/__init__.py +0 -0
  105. junifer/external/BrainPrint/brainprint/utils/tests/test_config.py +33 -0
  106. junifer/external/BrainPrint/doc/Makefile +20 -0
  107. junifer/external/BrainPrint/doc/_static/.gitkeep +0 -0
  108. junifer/external/BrainPrint/doc/_templates/autosummary/class.rst +10 -0
  109. junifer/external/BrainPrint/doc/_templates/autosummary/function.rst +8 -0
  110. junifer/external/BrainPrint/doc/_templates/autosummary/module.rst +6 -0
  111. junifer/external/BrainPrint/doc/api/index.rst +11 -0
  112. junifer/external/BrainPrint/doc/conf.py +272 -0
  113. junifer/external/BrainPrint/doc/index.rst +140 -0
  114. junifer/external/BrainPrint/doc/links.inc +36 -0
  115. junifer/external/BrainPrint/doc/make.bat +35 -0
  116. junifer/external/BrainPrint/doc/references.bib +23 -0
  117. junifer/external/BrainPrint/pyproject.toml +170 -0
  118. junifer/external/BrainPrint/setup.py +3 -0
  119. junifer/external/__init__.py +5 -0
  120. junifer/external/__init__.pyi +0 -0
  121. junifer/external/nilearn/junifer_connectivity_measure.py +95 -39
  122. junifer/external/nilearn/junifer_nifti_spheres_masker.py +226 -67
  123. junifer/external/nilearn/tests/test_junifer_connectivity_measure.py +39 -27
  124. junifer/external/nilearn/tests/test_junifer_nifti_spheres_masker.py +231 -21
  125. junifer/external/py.typed +0 -0
  126. junifer/markers/__init__.pyi +6 -3
  127. junifer/markers/base.py +126 -76
  128. junifer/markers/brainprint.py +30 -39
  129. junifer/markers/complexity/__init__.py +20 -3
  130. junifer/markers/complexity/complexity_base.py +40 -41
  131. junifer/markers/complexity/hurst_exponent.py +22 -35
  132. junifer/markers/complexity/multiscale_entropy_auc.py +53 -41
  133. junifer/markers/complexity/perm_entropy.py +22 -36
  134. junifer/markers/complexity/range_entropy.py +22 -35
  135. junifer/markers/complexity/range_entropy_auc.py +27 -36
  136. junifer/markers/complexity/sample_entropy.py +22 -36
  137. junifer/markers/complexity/tests/test_complexity_registry.py +48 -0
  138. junifer/markers/complexity/tests/test_hurst_exponent.py +47 -36
  139. junifer/markers/complexity/tests/test_multiscale_entropy_auc.py +50 -36
  140. junifer/markers/complexity/tests/test_perm_entropy.py +48 -36
  141. junifer/markers/complexity/tests/test_range_entropy.py +47 -36
  142. junifer/markers/complexity/tests/test_range_entropy_auc.py +47 -36
  143. junifer/markers/complexity/tests/test_sample_entropy.py +48 -36
  144. junifer/markers/complexity/tests/test_weighted_perm_entropy.py +50 -36
  145. junifer/markers/complexity/weighted_perm_entropy.py +22 -36
  146. junifer/markers/ets_rss.py +35 -31
  147. junifer/markers/falff/__init__.pyi +7 -1
  148. junifer/markers/falff/_afni_falff.py +22 -9
  149. junifer/markers/falff/_junifer_falff.py +47 -31
  150. junifer/markers/falff/falff_base.py +80 -60
  151. junifer/markers/falff/falff_maps.py +17 -41
  152. junifer/markers/falff/falff_parcels.py +26 -51
  153. junifer/markers/falff/falff_spheres.py +34 -61
  154. junifer/markers/falff/tests/test_falff_maps.py +20 -23
  155. junifer/markers/falff/tests/test_falff_mask.py +99 -0
  156. junifer/markers/falff/tests/test_falff_parcels.py +14 -10
  157. junifer/markers/falff/tests/test_falff_spheres.py +14 -10
  158. junifer/markers/functional_connectivity/crossparcellation_functional_connectivity.py +32 -34
  159. junifer/markers/functional_connectivity/edge_functional_connectivity_maps.py +12 -25
  160. junifer/markers/functional_connectivity/edge_functional_connectivity_parcels.py +19 -34
  161. junifer/markers/functional_connectivity/edge_functional_connectivity_spheres.py +24 -44
  162. junifer/markers/functional_connectivity/functional_connectivity_base.py +50 -40
  163. junifer/markers/functional_connectivity/functional_connectivity_maps.py +12 -25
  164. junifer/markers/functional_connectivity/functional_connectivity_parcels.py +19 -34
  165. junifer/markers/functional_connectivity/functional_connectivity_spheres.py +24 -45
  166. junifer/markers/functional_connectivity/tests/test_crossparcellation_functional_connectivity.py +21 -22
  167. junifer/markers/functional_connectivity/tests/test_edge_functional_connectivity_maps.py +8 -7
  168. junifer/markers/functional_connectivity/tests/test_edge_functional_connectivity_parcels.py +8 -4
  169. junifer/markers/functional_connectivity/tests/test_edge_functional_connectivity_spheres.py +8 -4
  170. junifer/markers/functional_connectivity/tests/test_functional_connectivity_maps.py +8 -7
  171. junifer/markers/functional_connectivity/tests/test_functional_connectivity_parcels.py +9 -5
  172. junifer/markers/functional_connectivity/tests/test_functional_connectivity_spheres.py +8 -12
  173. junifer/markers/maps_aggregation.py +77 -55
  174. junifer/markers/parcel_aggregation.py +91 -70
  175. junifer/markers/reho/__init__.pyi +7 -1
  176. junifer/markers/reho/_afni_reho.py +23 -14
  177. junifer/markers/reho/_junifer_reho.py +112 -123
  178. junifer/markers/reho/reho_base.py +115 -37
  179. junifer/markers/reho/reho_maps.py +18 -32
  180. junifer/markers/reho/reho_parcels.py +26 -41
  181. junifer/markers/reho/reho_spheres.py +33 -54
  182. junifer/markers/reho/tests/test_junifer_reho.py +178 -0
  183. junifer/markers/reho/tests/test_reho_maps.py +27 -25
  184. junifer/markers/reho/tests/test_reho_parcels.py +28 -22
  185. junifer/markers/reho/tests/test_reho_spheres.py +16 -12
  186. junifer/markers/sphere_aggregation.py +88 -72
  187. junifer/markers/temporal_snr/temporal_snr_base.py +32 -30
  188. junifer/markers/temporal_snr/temporal_snr_maps.py +9 -18
  189. junifer/markers/temporal_snr/temporal_snr_parcels.py +19 -27
  190. junifer/markers/temporal_snr/temporal_snr_spheres.py +27 -41
  191. junifer/markers/temporal_snr/tests/test_temporal_snr_base.py +2 -2
  192. junifer/markers/temporal_snr/tests/test_temporal_snr_maps.py +17 -22
  193. junifer/markers/temporal_snr/tests/test_temporal_snr_parcels.py +11 -3
  194. junifer/markers/temporal_snr/tests/test_temporal_snr_spheres.py +9 -9
  195. junifer/markers/tests/test_brainprint.py +10 -9
  196. junifer/markers/tests/test_ets_rss.py +7 -3
  197. junifer/markers/tests/test_lost_regions.py +279 -0
  198. junifer/markers/tests/test_maps_aggregation.py +67 -114
  199. junifer/markers/tests/test_marker_utils.py +40 -1
  200. junifer/markers/tests/test_markers_base.py +275 -6
  201. junifer/markers/tests/test_parcel_aggregation.py +91 -53
  202. junifer/markers/tests/test_sphere_aggregation.py +53 -47
  203. junifer/markers/utils.py +267 -5
  204. junifer/onthefly/__init__.py +2 -3
  205. junifer/onthefly/__init__.pyi +4 -0
  206. junifer/onthefly/_brainprint.py +3 -5
  207. junifer/onthefly/py.typed +0 -0
  208. junifer/onthefly/read_transform.py +11 -9
  209. junifer/onthefly/tests/test_read_transform.py +7 -7
  210. junifer/pipeline/__init__.pyi +4 -2
  211. junifer/pipeline/marker_collection.py +14 -10
  212. junifer/pipeline/pipeline_component_registry.py +25 -6
  213. junifer/pipeline/pipeline_step_mixin.py +5 -12
  214. junifer/pipeline/tests/test_data_object_dumper.py +10 -10
  215. junifer/pipeline/tests/test_marker_collection.py +145 -168
  216. junifer/pipeline/tests/test_pipeline_component_registry.py +52 -8
  217. junifer/pipeline/tests/test_pipeline_step_mixin.py +12 -26
  218. junifer/pipeline/tests/test_update_meta_mixin.py +4 -5
  219. junifer/pipeline/tests/test_workdir_manager.py +39 -0
  220. junifer/pipeline/update_meta_mixin.py +13 -10
  221. junifer/pipeline/utils.py +22 -17
  222. junifer/pipeline/workdir_manager.py +41 -32
  223. junifer/preprocess/__init__.pyi +9 -4
  224. junifer/preprocess/_temporal_filter.py +34 -33
  225. junifer/preprocess/_temporal_slicer.py +17 -30
  226. junifer/preprocess/base.py +99 -55
  227. junifer/preprocess/confounds/__init__.pyi +10 -2
  228. junifer/preprocess/confounds/fmriprep_confound_remover.py +87 -109
  229. junifer/preprocess/confounds/tests/test_fmriprep_confound_remover.py +64 -45
  230. junifer/preprocess/smoothing/__init__.pyi +2 -2
  231. junifer/preprocess/smoothing/_afni_smoothing.py +8 -7
  232. junifer/preprocess/smoothing/_fsl_smoothing.py +6 -4
  233. junifer/preprocess/smoothing/_nilearn_smoothing.py +7 -5
  234. junifer/preprocess/smoothing/smoothing.py +59 -40
  235. junifer/preprocess/smoothing/tests/test_smoothing.py +28 -8
  236. junifer/preprocess/tests/test_preprocess_base.py +17 -9
  237. junifer/preprocess/tests/test_temporal_filter.py +37 -16
  238. junifer/preprocess/tests/test_temporal_slicer.py +12 -12
  239. junifer/preprocess/warping/__init__.pyi +2 -2
  240. junifer/preprocess/warping/_ants_warper.py +56 -40
  241. junifer/preprocess/warping/_fsl_warper.py +49 -36
  242. junifer/preprocess/warping/_utils.py +106 -0
  243. junifer/preprocess/warping/space_warper.py +99 -98
  244. junifer/preprocess/warping/tests/test_space_warper.py +46 -41
  245. junifer/preprocess/warping/tests/test_warping_utils.py +163 -0
  246. junifer/stats.py +46 -34
  247. junifer/storage/__init__.pyi +8 -4
  248. junifer/storage/base.py +129 -64
  249. junifer/storage/hdf5.py +307 -199
  250. junifer/storage/pandas_base.py +46 -24
  251. junifer/storage/sqlite.py +99 -62
  252. junifer/storage/tests/data/collected_h5io_fork.hdf5 +0 -0
  253. junifer/storage/tests/test_hdf5.py +147 -12
  254. junifer/storage/tests/test_repeated_storage.py +293 -0
  255. junifer/storage/tests/test_sqlite.py +9 -20
  256. junifer/storage/tests/test_storage_base.py +18 -21
  257. junifer/storage/tests/test_utils.py +83 -22
  258. junifer/storage/utils.py +58 -38
  259. junifer/testing/__init__.pyi +2 -2
  260. junifer/testing/datagrabbers.py +480 -44
  261. junifer/testing/registry.py +12 -0
  262. junifer/testing/tests/test_adhdtesting_datagrabber.py +24 -0
  263. junifer/testing/tests/test_oasisvmbtesting_datagrabber.py +5 -0
  264. junifer/testing/tests/test_partlycloudytesting_datagrabber.py +52 -0
  265. junifer/testing/tests/test_spmauditory_datagrabber.py +16 -0
  266. junifer/testing/tests/test_testing_registry.py +31 -0
  267. junifer/testing/tests/test_testing_utils.py +35 -0
  268. junifer/testing/tests/test_uclacnpvbmtesting_datagrabber.py +81 -0
  269. junifer/testing/utils.py +33 -1
  270. junifer/tests/test_stats.py +1 -5
  271. junifer/typing/__init__.pyi +2 -0
  272. junifer/typing/_typing.py +34 -19
  273. junifer/typing/py.typed +0 -0
  274. junifer/utils/__init__.pyi +10 -1
  275. junifer/utils/_config.py +1 -2
  276. junifer/utils/helpers.py +68 -2
  277. junifer/utils/logging.py +31 -7
  278. junifer/utils/tests/test_helpers.py +36 -1
  279. {junifer-0.0.7.dev332.dist-info → junifer-0.0.8.dist-info}/METADATA +43 -35
  280. junifer-0.0.8.dist-info/RECORD +402 -0
  281. {junifer-0.0.7.dev332.dist-info → junifer-0.0.8.dist-info}/WHEEL +1 -1
  282. junifer/cli/tests/test_parser.py +0 -280
  283. junifer/external/h5io/h5io/__init__.py +0 -15
  284. junifer/external/h5io/h5io/_h5io.py +0 -767
  285. junifer/external/h5io/h5io/_version.py +0 -1
  286. junifer/external/h5io/h5io/chunked_array.py +0 -94
  287. junifer/external/h5io/h5io/chunked_list.py +0 -65
  288. junifer/markers/complexity/__init__.pyi +0 -17
  289. junifer-0.0.7.dev332.dist-info/RECORD +0 -353
  290. {junifer-0.0.7.dev332.dist-info → junifer-0.0.8.dist-info}/entry_points.txt +0 -0
  291. {junifer-0.0.7.dev332.dist-info → junifer-0.0.8.dist-info}/licenses/AUTHORS.rst +0 -0
  292. {junifer-0.0.7.dev332.dist-info → junifer-0.0.8.dist-info}/licenses/LICENSE.md +0 -0
  293. {junifer-0.0.7.dev332.dist-info → junifer-0.0.8.dist-info}/top_level.txt +0 -0
@@ -6,19 +6,36 @@
6
6
  # License: AGPL
7
7
 
8
8
  import logging
9
+ import sys
10
+ import tempfile
9
11
  from contextlib import AbstractContextManager, nullcontext
12
+ from copy import deepcopy
10
13
  from pathlib import Path
11
- from typing import Any, Optional, Union
14
+ from typing import Any, ClassVar
12
15
 
16
+ import nibabel as nib
17
+ import numpy as np
13
18
  import pytest
14
19
  from nibabel.filebasedimages import ImageFileError
15
20
  from ruamel.yaml import YAML
16
21
 
17
22
  import junifer.testing.registry # noqa: F401
18
- from junifer.api import collect, list_elements, queue, reset, run
23
+ from junifer.api import collect, list_elements, parse_yaml, queue, reset, run
24
+ from junifer.api.decorators import register_marker
25
+ from junifer.datagrabber import DataType, PatternDataGrabber
19
26
  from junifer.datagrabber.base import BaseDataGrabber
20
- from junifer.pipeline import PipelineComponentRegistry
21
- from junifer.typing import Elements
27
+ from junifer.markers import BaseMarker
28
+ from junifer.pipeline import (
29
+ MarkerCollection,
30
+ PipelineComponentRegistry,
31
+ WorkDirManager,
32
+ )
33
+ from junifer.storage import StorageType
34
+ from junifer.storage.base import BaseFeatureStorage
35
+ from junifer.typing import Dependencies, Elements, MarkerInOutMappings
36
+
37
+
38
+ pytestmark = pytest.mark.external
22
39
 
23
40
 
24
41
  # Configure YAML class
@@ -31,10 +48,14 @@ yaml.indent(mapping=2, sequence=4, offset=2)
31
48
  # Kept for parametrizing
32
49
  _datagrabber = {
33
50
  "kind": "PartlyCloudyTestingDataGrabber",
51
+ # Fewer subjects for faster tests
52
+ "n_subjects": 4,
34
53
  }
35
54
  _bids_ses_datagrabber = {
36
55
  "kind": "PatternDataladDataGrabber",
37
- "uri": "https://gin.g-node.org/juaml/datalad-example-bids-ses",
56
+ "uri": (
57
+ "https://cerebra.fz-juelich.de/junifer/datalad-example-bids-ses.git"
58
+ ),
38
59
  "types": ["T1w", "BOLD"],
39
60
  "patterns": {
40
61
  "T1w": {
@@ -62,7 +83,7 @@ def datagrabber() -> dict[str, str]:
62
83
 
63
84
 
64
85
  @pytest.fixture
65
- def markers() -> list[dict[str, str]]:
86
+ def markers() -> list[dict[str, list[str] | str]]:
66
87
  """Return markers as a list of dictionary."""
67
88
  return [
68
89
  {
@@ -84,18 +105,15 @@ def markers() -> list[dict[str, str]]:
84
105
  def storage() -> dict[str, str]:
85
106
  """Return a storage as a dictionary."""
86
107
  return {
87
- "kind": "SQLiteFeatureStorage",
108
+ "kind": "HDF5FeatureStorage",
88
109
  }
89
110
 
90
111
 
91
112
  @pytest.mark.parametrize(
92
113
  "datagrabber, element, expect",
93
114
  [
94
- (
95
- _datagrabber,
96
- [("sub-01",)],
97
- pytest.raises(RuntimeError, match="element selectors are invalid"),
98
- ),
115
+ # A complete element
116
+ (_datagrabber, [("sub-01",)], nullcontext()),
99
117
  (
100
118
  _datagrabber,
101
119
  ["sub-01"],
@@ -111,15 +129,16 @@ def storage() -> dict[str, str]:
111
129
  [("sub-01", "ses-01")],
112
130
  pytest.raises(ImageFileError, match="is not a gzip file"),
113
131
  ),
132
+ # Complete elements without data
114
133
  (
115
134
  _bids_ses_datagrabber,
116
135
  [("sub-01", "ses-100")],
117
- pytest.raises(RuntimeError, match="element selectors are invalid"),
136
+ pytest.raises(RuntimeError, match="Cannot access"),
118
137
  ),
119
138
  (
120
139
  _bids_ses_datagrabber,
121
140
  [("sub-100", "ses-01")],
122
- pytest.raises(RuntimeError, match="element selectors are invalid"),
141
+ pytest.raises(RuntimeError, match="Cannot access"),
123
142
  ),
124
143
  ],
125
144
  )
@@ -150,7 +169,7 @@ def test_run_single_element(
150
169
 
151
170
  """
152
171
  # Set storage
153
- storage["uri"] = str((tmp_path / "out.sqlite").resolve())
172
+ storage["uri"] = str((tmp_path / "out.hdf5").resolve())
154
173
  # Run operations
155
174
  with expect:
156
175
  run(
@@ -161,7 +180,7 @@ def test_run_single_element(
161
180
  elements=element,
162
181
  )
163
182
  # Check files
164
- files = list(tmp_path.glob("*.sqlite"))
183
+ files = list(tmp_path.glob("*.hdf5"))
165
184
  assert len(files) == 1
166
185
 
167
186
 
@@ -183,7 +202,7 @@ def test_run_single_element_with_preprocessing(
183
202
 
184
203
  """
185
204
  # Set storage
186
- storage["uri"] = str((tmp_path / "out.sqlite").resolve())
205
+ storage["uri"] = str((tmp_path / "out.hdf5").resolve())
187
206
  # Run operations
188
207
  run(
189
208
  workdir={"path": tmp_path, "cleanup": False},
@@ -201,17 +220,14 @@ def test_run_single_element_with_preprocessing(
201
220
  elements=["sub-01"],
202
221
  )
203
222
  # Check files
204
- files = list(tmp_path.glob("*.sqlite"))
223
+ files = list(tmp_path.glob("*.hdf5"))
205
224
  assert len(files) == 1
206
225
 
207
226
 
208
227
  @pytest.mark.parametrize(
209
228
  "element, expect",
210
229
  [
211
- (
212
- [("sub-01",), ("sub-03",)],
213
- pytest.raises(RuntimeError, match="element selectors are invalid"),
214
- ),
230
+ ([("sub-01",), ("sub-03",)], nullcontext()),
215
231
  (["sub-01", "sub-03"], nullcontext()),
216
232
  ],
217
233
  )
@@ -242,7 +258,7 @@ def test_run_multi_element_multi_output(
242
258
 
243
259
  """
244
260
  # Set storage
245
- storage["uri"] = str((tmp_path / "out.sqlite").resolve())
261
+ storage["uri"] = str((tmp_path / "out.hdf5").resolve())
246
262
  storage["single_output"] = False # type: ignore
247
263
  # Run operations
248
264
  with expect:
@@ -254,7 +270,7 @@ def test_run_multi_element_multi_output(
254
270
  elements=element,
255
271
  )
256
272
  # Check files
257
- files = list(tmp_path.glob("*.sqlite"))
273
+ files = list(tmp_path.glob("*.hdf5"))
258
274
  assert len(files) == 2
259
275
 
260
276
 
@@ -279,7 +295,7 @@ def test_run_multi_element_single_output(
279
295
 
280
296
  """
281
297
  # Set storage
282
- storage["uri"] = str((tmp_path / "out.sqlite").resolve())
298
+ storage["uri"] = str((tmp_path / "out.hdf5").resolve())
283
299
  storage["single_output"] = True # type: ignore
284
300
  # Run operations
285
301
  run(
@@ -290,9 +306,9 @@ def test_run_multi_element_single_output(
290
306
  elements=["sub-01", "sub-03"],
291
307
  )
292
308
  # Check files
293
- files = list(tmp_path.glob("*.sqlite"))
309
+ files = list(tmp_path.glob("*.hdf5"))
294
310
  assert len(files) == 1
295
- assert files[0].name == "out.sqlite"
311
+ assert files[0].name == "out.hdf5"
296
312
 
297
313
 
298
314
  def test_run_and_collect(
@@ -316,7 +332,7 @@ def test_run_and_collect(
316
332
 
317
333
  """
318
334
  # Set storage
319
- uri = tmp_path / "out.sqlite"
335
+ uri = tmp_path / "out.hdf5"
320
336
  storage["uri"] = str(uri.resolve())
321
337
  storage["single_output"] = False # type: ignore
322
338
  # Run operations
@@ -328,13 +344,16 @@ def test_run_and_collect(
328
344
  )
329
345
  # Get datagrabber
330
346
  dg = PipelineComponentRegistry().build_component_instance(
331
- step="datagrabber", name=datagrabber["kind"], baseclass=BaseDataGrabber
347
+ step="datagrabber",
348
+ name=datagrabber["kind"],
349
+ baseclass=BaseDataGrabber,
350
+ init_params={k: v for k, v in datagrabber.items() if k != "kind"},
332
351
  )
333
352
  elements = dg.get_elements() # type: ignore
334
- # This should create 10 files
335
- files = list(tmp_path.glob("*.sqlite"))
353
+ # This should create one file per element
354
+ files = list(tmp_path.glob("*.hdf5"))
336
355
  assert len(files) == len(elements)
337
- # But the test.sqlite file should not exist
356
+ # But the test.hdf5 file should not exist
338
357
  assert not uri.exists()
339
358
  # Collect in storage
340
359
  collect(storage)
@@ -516,7 +535,7 @@ def test_queue_with_imports(
516
535
  tmp_path: Path,
517
536
  monkeypatch: pytest.MonkeyPatch,
518
537
  caplog: pytest.LogCaptureFixture,
519
- with_: Union[str, list[str]],
538
+ with_: str | list[str],
520
539
  ) -> None:
521
540
  """Test queue with `with` imports.
522
541
 
@@ -628,6 +647,143 @@ def test_queue_without_elements(
628
647
  assert "Queue done" in caplog.text
629
648
 
630
649
 
650
+ def _workdir(tmp_path: Path, as_dict: bool) -> tuple[str | dict, Path]:
651
+ """Get a working directory in the test directory.
652
+
653
+ Parameters
654
+ ----------
655
+ tmp_path : pathlib.Path
656
+ The path to the test directory.
657
+ as_dict : bool
658
+ Whether to give the working directory as a dictionary or a path.
659
+
660
+ Returns
661
+ -------
662
+ str or dict
663
+ The working directory, as given to the functions.
664
+ pathlib.Path
665
+ The path to the working directory.
666
+
667
+ """
668
+ path = tmp_path / "workdir"
669
+ workdir = {"path": str(path), "cleanup": True} if as_dict else str(path)
670
+ return workdir, path
671
+
672
+
673
+ @pytest.mark.parametrize("as_dict", [False, True])
674
+ @pytest.mark.parametrize("elements", [None, ["sub-01"]])
675
+ def test_queue_workdir(
676
+ tmp_path: Path,
677
+ monkeypatch: pytest.MonkeyPatch,
678
+ datagrabber: dict[str, str],
679
+ elements: list[str] | None,
680
+ as_dict: bool,
681
+ ) -> None:
682
+ """Test queue sets up the working directory.
683
+
684
+ Parameters
685
+ ----------
686
+ tmp_path : pathlib.Path
687
+ The path to the test directory.
688
+ monkeypatch : pytest.MonkeyPatch
689
+ The pytest.MonkeyPatch object.
690
+ datagrabber : dict
691
+ Testing datagrabber as dictionary.
692
+ elements : list of str or None
693
+ The parametrized elements to queue. If None, the elements are
694
+ listed using the datagrabber.
695
+ as_dict : bool
696
+ Whether to give the working directory as a dictionary or a path.
697
+
698
+ """
699
+ workdir, path = _workdir(tmp_path, as_dict)
700
+ with monkeypatch.context() as m:
701
+ m.chdir(tmp_path)
702
+ queue(
703
+ config={"workdir": workdir, "datagrabber": datagrabber},
704
+ kind="HTCondor",
705
+ elements=elements,
706
+ )
707
+ assert WorkDirManager().workdir == path
708
+ assert path.is_dir()
709
+ # The working directory is written to the job configuration as given
710
+ config = YAML().load(tmp_path / "junifer_jobs/junifer_job/config.yaml")
711
+ assert config["workdir"] == workdir
712
+
713
+
714
+ def test_queue_workdir_cleanup(
715
+ tmp_path: Path,
716
+ monkeypatch: pytest.MonkeyPatch,
717
+ datagrabber: dict[str, str],
718
+ ) -> None:
719
+ """Test queued jobs clean up the working directory.
720
+
721
+ Disabling the cleanup only applies to queueing (e.g. to debug it), as
722
+ the jobs would otherwise leave the files of each element.
723
+
724
+ Parameters
725
+ ----------
726
+ tmp_path : pathlib.Path
727
+ The path to the test directory.
728
+ monkeypatch : pytest.MonkeyPatch
729
+ The pytest.MonkeyPatch object.
730
+ datagrabber : dict
731
+ Testing datagrabber as dictionary.
732
+
733
+ """
734
+ workdir = {"path": str(tmp_path / "workdir"), "cleanup": False}
735
+ with monkeypatch.context() as m:
736
+ m.chdir(tmp_path)
737
+ with pytest.warns(RuntimeWarning, match="will be set to True"):
738
+ queue(
739
+ config={"workdir": workdir, "datagrabber": datagrabber},
740
+ kind="HTCondor",
741
+ )
742
+ # Queueing does not clean up
743
+ assert WorkDirManager()._cleanup_dirs is False
744
+ # The jobs clean up
745
+ config = YAML().load(tmp_path / "junifer_jobs/junifer_job/config.yaml")
746
+ assert config["workdir"]["cleanup"] is True
747
+
748
+
749
+ @pytest.mark.parametrize("as_dict", [False, True])
750
+ def test_collect_workdir(tmp_path: Path, as_dict: bool) -> None:
751
+ """Test collect sets up the working directory.
752
+
753
+ Parameters
754
+ ----------
755
+ tmp_path : pathlib.Path
756
+ The path to the test directory.
757
+ as_dict : bool
758
+ Whether to give the working directory as a dictionary or a path.
759
+
760
+ """
761
+ workdir, path = _workdir(tmp_path, as_dict)
762
+ storage = {"kind": "HDF5FeatureStorage", "uri": str(tmp_path / "out.hdf5")}
763
+ collect(storage, workdir=workdir)
764
+ assert WorkDirManager().workdir == path
765
+ assert path.is_dir()
766
+
767
+
768
+ @pytest.mark.parametrize("as_dict", [False, True])
769
+ def test_reset_workdir(tmp_path: Path, as_dict: bool) -> None:
770
+ """Test reset sets up the working directory.
771
+
772
+ Parameters
773
+ ----------
774
+ tmp_path : pathlib.Path
775
+ The path to the test directory.
776
+ as_dict : bool
777
+ Whether to give the working directory as a dictionary or a path.
778
+
779
+ """
780
+ workdir, path = _workdir(tmp_path, as_dict)
781
+ storage = {"uri": str(tmp_path / "out.hdf5")}
782
+ reset(config={"workdir": workdir, "storage": storage})
783
+ assert WorkDirManager().workdir == path
784
+ assert path.is_dir()
785
+
786
+
631
787
  def test_reset_run(
632
788
  tmp_path: Path,
633
789
  datagrabber: dict[str, str],
@@ -649,7 +805,7 @@ def test_reset_run(
649
805
 
650
806
  """
651
807
  # Create storage
652
- storage["uri"] = tmp_path / "test_reset_run.sqlite" # type: ignore
808
+ storage["uri"] = tmp_path / "test_reset_run.hdf5" # type: ignore
653
809
  # Run operation to generate files
654
810
  run(
655
811
  workdir=tmp_path,
@@ -700,7 +856,7 @@ def test_reset_queue(
700
856
  with monkeypatch.context() as m:
701
857
  m.chdir(tmp_path)
702
858
  # Create storage
703
- storage["uri"] = "test_reset_queue.sqlite"
859
+ storage["uri"] = "test_reset_queue.hdf5"
704
860
  # Set job name
705
861
  if job_name is None:
706
862
  job_name = "junifer_job"
@@ -742,7 +898,7 @@ def test_reset_queue(
742
898
  )
743
899
  def test_list_elements(
744
900
  datagrabber: dict[str, str],
745
- elements: Optional[list[tuple[str, ...]]],
901
+ elements: list[tuple[str, ...]] | None,
746
902
  ) -> None:
747
903
  """Test elements listing.
748
904
 
@@ -756,3 +912,674 @@ def test_list_elements(
756
912
  """
757
913
  listed_elements = list_elements(datagrabber, elements)
758
914
  assert "sub-01" in listed_elements
915
+
916
+
917
+ @pytest.mark.parametrize("as_dict", [False, True, None])
918
+ def test_list_elements_workdir(
919
+ tmp_path: Path,
920
+ monkeypatch: pytest.MonkeyPatch,
921
+ datagrabber: dict[str, str],
922
+ as_dict: bool | None,
923
+ ) -> None:
924
+ """Test elements listing sets up the working directory.
925
+
926
+ Parameters
927
+ ----------
928
+ tmp_path : pathlib.Path
929
+ The path to the test directory.
930
+ monkeypatch : pytest.MonkeyPatch
931
+ The pytest.MonkeyPatch object.
932
+ datagrabber : dict
933
+ Testing datagrabber as dictionary.
934
+ as_dict : bool or None
935
+ Whether to give the working directory as a dictionary or a path.
936
+ If None, no working directory is given and the default is used.
937
+
938
+ """
939
+ if as_dict is None:
940
+ # The default working directory is in the temporary directory
941
+ monkeypatch.setattr(tempfile, "tempdir", str(tmp_path))
942
+ workdir, path = None, tmp_path / "junifer"
943
+ else:
944
+ workdir, path = _workdir(tmp_path, as_dict)
945
+ listed_elements = list_elements(datagrabber, workdir=workdir)
946
+ assert "sub-01" in listed_elements
947
+ assert WorkDirManager().workdir == path
948
+ assert path.is_dir()
949
+
950
+
951
+ def test_parse_yaml_failure() -> None:
952
+ """Test YAML parsing failure."""
953
+ with pytest.raises(ValueError, match="does not exist"):
954
+ parse_yaml("foo.yaml")
955
+
956
+
957
+ def test_parse_yaml_empty_elements_failure(tmp_path: Path) -> None:
958
+ """Test YAML parsing with empty elements failure.
959
+
960
+ Parameters
961
+ ----------
962
+ tmp_path : pathlib.Path
963
+ The path to the test directory.
964
+
965
+ """
966
+ # Write test file
967
+ fname = tmp_path / "test_parse_yaml_empty_elements_failure.yaml"
968
+ fname.write_text("elements:")
969
+ # Check test file
970
+ with pytest.raises(ValueError, match="elements key was defined"):
971
+ parse_yaml(fname)
972
+
973
+
974
+ def test_parse_yaml_success(tmp_path: Path) -> None:
975
+ """Test YAML parsing success.
976
+
977
+ Parameters
978
+ ----------
979
+ tmp_path : pathlib.Path
980
+ The path to the test directory.
981
+
982
+ """
983
+ # Write test file
984
+ fname = tmp_path / "test_parse_yaml_success.yaml"
985
+ fname.write_text("foo: bar")
986
+ # Check test file
987
+ contents = parse_yaml(fname)
988
+ assert "foo" in contents
989
+ assert contents["foo"] == "bar"
990
+
991
+
992
+ def test_parse_yaml_success_with_module_autoload(tmp_path: Path) -> None:
993
+ """Test YAML parsing with single module autoload success.
994
+
995
+ Parameters
996
+ ----------
997
+ tmp_path : pathlib.Path
998
+ The path to the test directory.
999
+
1000
+ """
1001
+ # Write test file
1002
+ fname = tmp_path / "test_parse_yaml_with_single_module_autoload.yaml"
1003
+ fname.write_text("foo: bar\nwith: numpy")
1004
+ # Check test file
1005
+ contents = parse_yaml(fname)
1006
+ assert "foo" in contents
1007
+ assert contents["foo"] == "bar"
1008
+ assert "with" in contents
1009
+ assert contents["with"] == ["numpy"]
1010
+ assert "numpy" in sys.modules
1011
+ assert "junifer.configs.wrong_config" not in sys.modules
1012
+
1013
+
1014
+ def test_parse_yaml_failure_with_multi_module_autoload(tmp_path: Path) -> None:
1015
+ """Test YAML parsing with multi module autoload failure.
1016
+
1017
+ Parameters
1018
+ ----------
1019
+ tmp_path : pathlib.Path
1020
+ The path to the test directory.
1021
+
1022
+ """
1023
+ # Write test file
1024
+ fname = tmp_path / "test_parse_yaml_with_multi_module_autoload.yaml"
1025
+ fname.write_text(
1026
+ "foo: bar\nwith:\n - numpy\n - junifer.testing.wrong_config"
1027
+ )
1028
+ # Check test file
1029
+ with pytest.raises(ImportError, match="wrong_config"):
1030
+ parse_yaml(fname)
1031
+
1032
+
1033
+ def test_parse_yaml_with_wrong_path(tmp_path: Path) -> None:
1034
+ """Test YAML parsing with wrong paths in with.
1035
+
1036
+ Parameters
1037
+ ----------
1038
+ tmp_path : pathlib.Path
1039
+ The path to the test directory.
1040
+
1041
+ """
1042
+ t_tmp_path = tmp_path / "test_relative_with"
1043
+ # Write yaml that includes a relative path
1044
+ yaml_path = t_tmp_path / "yamls"
1045
+ yaml_path.mkdir(exist_ok=True, parents=True)
1046
+ yaml_fname = yaml_path / "test_parse_yaml_wrong_path.yaml"
1047
+
1048
+ yaml_fname.write_text("foo: bar\nwith:\n - missingt.py\n - scipy\n")
1049
+
1050
+ # Check test file
1051
+ with pytest.raises(ValueError, match="does not exist"):
1052
+ parse_yaml(yaml_fname)
1053
+
1054
+
1055
+ def test_parse_yaml_relative_path(tmp_path: Path) -> None:
1056
+ """Test YAML parsing with relative paths in with.
1057
+
1058
+ Parameters
1059
+ ----------
1060
+ tmp_path : pathlib.Path
1061
+ The path to the test directory.
1062
+
1063
+ """
1064
+ t_tmp_path = tmp_path / "test_relative_with"
1065
+
1066
+ # Write .py to include
1067
+ py_path = t_tmp_path / "external"
1068
+ py_path.mkdir(exist_ok=True, parents=True)
1069
+ py_fname = py_path / "first.py"
1070
+ py_fname.write_text("import numpy as np\n")
1071
+
1072
+ # Write yaml that includes a relative path
1073
+ yaml_path = t_tmp_path / "yamls"
1074
+ yaml_path.mkdir(exist_ok=True, parents=True)
1075
+ yaml_fname = yaml_path / "test_parse_yaml_relative_path.yaml"
1076
+
1077
+ yaml_fname.write_text(
1078
+ "foo: bar\nwith:\n - ../external/first.py\n - scipy\n"
1079
+ )
1080
+
1081
+ # Check test file
1082
+ parse_yaml(yaml_fname)
1083
+
1084
+
1085
+ def test_parse_yaml_absolute_path(tmp_path: Path) -> None:
1086
+ """Test YAML parsing with absolute paths in with.
1087
+
1088
+ Parameters
1089
+ ----------
1090
+ tmp_path : pathlib.Path
1091
+ The path to the test directory.
1092
+
1093
+ """
1094
+ t_tmp_path = tmp_path / "test_relative_with"
1095
+
1096
+ # Write .py to include
1097
+ py_path = t_tmp_path / "external"
1098
+ py_path.mkdir(exist_ok=True, parents=True)
1099
+ py_fname = py_path / "first.py"
1100
+ py_fname.write_text("import numpy as np\n")
1101
+
1102
+ # Write yaml that includes a relative path
1103
+ yaml_path = t_tmp_path / "yamls"
1104
+ yaml_path.mkdir(exist_ok=True, parents=True)
1105
+ yaml_fname = yaml_path / "test_parse_yaml_relative_path.yaml"
1106
+
1107
+ yaml_fname.write_text(
1108
+ f"foo: bar\nwith:\n - {py_fname.absolute()}\n - scipy\n"
1109
+ )
1110
+
1111
+ # Check test file
1112
+ parse_yaml(yaml_fname)
1113
+
1114
+
1115
+ def test_parse_yaml_multi_module_deps(tmp_path: Path) -> None:
1116
+ """Test YAML parsing with multi-module import with deps.
1117
+
1118
+ Parameters
1119
+ ----------
1120
+ tmp_path : pathlib.Path
1121
+ The path to the test directory.
1122
+
1123
+ """
1124
+ t_tmp_path = tmp_path / "test_with_multi_module"
1125
+
1126
+ # Write .py to include
1127
+ py_path = t_tmp_path / "external"
1128
+ py_path.mkdir(exist_ok=True, parents=True)
1129
+ py_fname_1 = py_path / "first.py"
1130
+ py_fname_1.write_text(
1131
+ "import numpy as np\nfrom second import hej\n"
1132
+ "def junifer_module_deps(): return ['second.py']\n"
1133
+ )
1134
+ py_fname_2 = py_path / "second.py"
1135
+ py_fname_2.write_text("def hej(): print('hej')\n")
1136
+
1137
+ # Write yaml
1138
+ yaml_path = t_tmp_path / "yamls"
1139
+ yaml_path.mkdir(exist_ok=True, parents=True)
1140
+ yaml_fname = yaml_path / "test_parse_yaml_multi_module.yaml"
1141
+
1142
+ yaml_fname.write_text(
1143
+ "foo: bar\nwith:\n - ../external/first.py\n - scipy\n"
1144
+ )
1145
+
1146
+ # Check test file
1147
+ parse_yaml(yaml_fname)
1148
+
1149
+
1150
+ def test_parse_storage_uri_relative(tmp_path: Path) -> None:
1151
+ """Test YAML parsing with storage and relative URI.
1152
+
1153
+ Parameters
1154
+ ----------
1155
+ tmp_path : pathlib.Path
1156
+ The path to the test directory.
1157
+
1158
+ """
1159
+ fname = tmp_path / "test_parse_yaml_with_storage_uri.yaml"
1160
+ fname.write_text("foo: bar\nwith: numpy\nstorage:\n uri: test.db\n")
1161
+
1162
+ contents = parse_yaml(fname)
1163
+ assert "foo" in contents
1164
+ assert contents["foo"] == "bar"
1165
+ assert "storage" in contents
1166
+ assert "uri" in contents["storage"]
1167
+ assert contents["storage"]["uri"] == str(tmp_path / "test.db")
1168
+
1169
+ fname = tmp_path / "test_parse_yaml_with_storage_uri.yaml"
1170
+ fname.write_text(
1171
+ "foo: bar\nwith: numpy\nstorage:\n uri: ../another/test.db\n"
1172
+ )
1173
+
1174
+ contents = parse_yaml(fname)
1175
+ assert "foo" in contents
1176
+ assert contents["foo"] == "bar"
1177
+ assert "storage" in contents
1178
+ assert "uri" in contents["storage"]
1179
+ assert contents["storage"]["uri"] == str(
1180
+ (tmp_path / "../another/test.db").resolve()
1181
+ )
1182
+
1183
+ fname = tmp_path / "test_parse_yaml_with_storage_uri.yaml"
1184
+ fname.write_text(
1185
+ "foo: bar\nwith: numpy\nstorage:\n uri: /absolute/test.db\n"
1186
+ )
1187
+
1188
+ contents = parse_yaml(fname)
1189
+ assert "foo" in contents
1190
+ assert contents["foo"] == "bar"
1191
+ assert "storage" in contents
1192
+ assert "uri" in contents["storage"]
1193
+ assert contents["storage"]["uri"] == "/absolute/test.db"
1194
+
1195
+ # Just to trick coverage
1196
+ fname = tmp_path / "test_parse_yaml_with_storage_uri.yaml"
1197
+ fname.write_text("foo: bar\nwith: numpy\nstorage:\n kind: SomeStorage\n")
1198
+
1199
+ contents = parse_yaml(fname)
1200
+ assert "foo" in contents
1201
+ assert contents["foo"] == "bar"
1202
+ assert "storage" in contents
1203
+
1204
+
1205
+ @pytest.mark.parametrize(
1206
+ "datadir, expected",
1207
+ [
1208
+ # Relative to the YAML file
1209
+ ("data", "data"),
1210
+ ("../other/data", "../other/data"),
1211
+ # Absolute
1212
+ ("/absolute/data", "/absolute/data"),
1213
+ ],
1214
+ )
1215
+ def test_parse_yaml_datadir(
1216
+ tmp_path: Path, datadir: str, expected: str
1217
+ ) -> None:
1218
+ """Test YAML parsing with the data directory of the datagrabber.
1219
+
1220
+ Parameters
1221
+ ----------
1222
+ tmp_path : pathlib.Path
1223
+ The path to the test directory.
1224
+ datadir : str
1225
+ The parametrized data directory.
1226
+ expected : str
1227
+ The parametrized data directory, relative to the YAML file.
1228
+
1229
+ """
1230
+ fname = tmp_path / "config.yaml"
1231
+ yaml.dump(
1232
+ {"datagrabber": {"kind": "DG", "datadir": datadir}}, stream=fname
1233
+ )
1234
+ contents = parse_yaml(fname)
1235
+ assert contents["datagrabber"]["datadir"] == str(
1236
+ (tmp_path / expected).resolve()
1237
+ )
1238
+
1239
+
1240
+ def test_parse_yaml_datadir_multiple(tmp_path: Path) -> None:
1241
+ """Test YAML parsing with the data directories of multiple datagrabbers.
1242
+
1243
+ Parameters
1244
+ ----------
1245
+ tmp_path : pathlib.Path
1246
+ The path to the test directory.
1247
+
1248
+ """
1249
+ fname = tmp_path / "config.yaml"
1250
+ datagrabber = {
1251
+ "kind": "MultipleDataGrabber",
1252
+ "datagrabbers": [
1253
+ {"kind": "DG", "datadir": "data"},
1254
+ {"kind": "DG", "datadir": "/absolute/data"},
1255
+ {"kind": "DG"},
1256
+ ],
1257
+ }
1258
+ yaml.dump({"datagrabber": datagrabber}, stream=fname)
1259
+ contents = parse_yaml(fname)
1260
+ datagrabbers = contents["datagrabber"]["datagrabbers"]
1261
+ assert datagrabbers[0]["datadir"] == str(tmp_path / "data")
1262
+ assert datagrabbers[1]["datadir"] == "/absolute/data"
1263
+ assert "datadir" not in datagrabbers[2]
1264
+
1265
+
1266
+ def test_parse_yaml_queue_venv_relative(tmp_path: Path) -> None:
1267
+ """Test YAML parsing with relative venv queue.
1268
+
1269
+ Parameters
1270
+ ----------
1271
+ tmp_path : pathlib.Path
1272
+ The path to the test directory.
1273
+
1274
+ """
1275
+ fname = tmp_path / "test_parse_yaml_queue_venv_relative.yaml"
1276
+ fname.write_text("queue:\n env:\n kind: venv\n name: .venv\n")
1277
+ _ = parse_yaml(fname)
1278
+
1279
+
1280
+ @pytest.mark.parametrize(
1281
+ "elements, expected, searches",
1282
+ [
1283
+ # Complete elements, without searching the dataset
1284
+ ([("sub-01", "rest")], [("sub-01", "rest")], 0),
1285
+ (
1286
+ [("sub-01", "rest"), ("sub-02", "movie")],
1287
+ [("sub-01", "rest"), ("sub-02", "movie")],
1288
+ 0,
1289
+ ),
1290
+ # Partial elements: all the tasks of a subject
1291
+ (["sub-01"], [("sub-01", "movie"), ("sub-01", "rest")], 1),
1292
+ ([("sub-01",)], [("sub-01", "movie"), ("sub-01", "rest")], 1),
1293
+ # Without duplicates
1294
+ (
1295
+ [("sub-01", "rest"), ("sub-01", "rest")],
1296
+ [("sub-01", "rest")],
1297
+ 0,
1298
+ ),
1299
+ # ("sub-01", "rest") matches both selectors
1300
+ (
1301
+ ["sub-01", "rest"],
1302
+ [("sub-01", "movie"), ("sub-01", "rest"), ("sub-02", "rest")],
1303
+ 1,
1304
+ ),
1305
+ ],
1306
+ )
1307
+ def test_run_element_selectors(
1308
+ tmp_path: Path,
1309
+ monkeypatch: pytest.MonkeyPatch,
1310
+ markers: list[dict[str, str]],
1311
+ elements: Elements,
1312
+ expected: list[tuple[str, str]],
1313
+ searches: int,
1314
+ ) -> None:
1315
+ """Test run function with complete and partial element selectors.
1316
+
1317
+ Parameters
1318
+ ----------
1319
+ tmp_path : pathlib.Path
1320
+ The path to the test directory.
1321
+ monkeypatch : pytest.MonkeyPatch
1322
+ The pytest.MonkeyPatch object.
1323
+ markers : list of dict
1324
+ Testing markers as list of dictionary.
1325
+ elements : list of str or tuple
1326
+ The parametrized element selectors.
1327
+ expected : list of tuple of str
1328
+ The parametrized elements to compute.
1329
+ searches : int
1330
+ The parametrized number of searches of the elements of the dataset.
1331
+
1332
+ """
1333
+ computed = []
1334
+ n_searches = _run_selectors(
1335
+ tmp_path, monkeypatch, markers, elements, computed
1336
+ )
1337
+ assert sorted(computed) == expected
1338
+ assert n_searches == searches
1339
+
1340
+
1341
+ def test_run_element_selectors_invalid(
1342
+ tmp_path: Path,
1343
+ monkeypatch: pytest.MonkeyPatch,
1344
+ markers: list[dict[str, str]],
1345
+ ) -> None:
1346
+ """Test run function with partial selectors matching no element.
1347
+
1348
+ Parameters
1349
+ ----------
1350
+ tmp_path : pathlib.Path
1351
+ The path to the test directory.
1352
+ monkeypatch : pytest.MonkeyPatch
1353
+ The pytest.MonkeyPatch object.
1354
+ markers : list of dict
1355
+ Testing markers as list of dictionary.
1356
+
1357
+ """
1358
+ computed = []
1359
+ with pytest.raises(RuntimeError, match=r"invalid:\n\['sub-03'\]"):
1360
+ _run_selectors(
1361
+ tmp_path, monkeypatch, markers, ["sub-01", "sub-03"], computed
1362
+ )
1363
+ # Nothing is computed, not even the elements of the valid selectors
1364
+ assert computed == []
1365
+
1366
+
1367
+ def test_run_element_selectors_sizes(
1368
+ tmp_path: Path,
1369
+ monkeypatch: pytest.MonkeyPatch,
1370
+ markers: list[dict[str, str]],
1371
+ ) -> None:
1372
+ """Test run function with selectors of different numbers of values.
1373
+
1374
+ Parameters
1375
+ ----------
1376
+ tmp_path : pathlib.Path
1377
+ The path to the test directory.
1378
+ monkeypatch : pytest.MonkeyPatch
1379
+ The pytest.MonkeyPatch object.
1380
+ markers : list of dict
1381
+ Testing markers as list of dictionary.
1382
+
1383
+ """
1384
+ computed = []
1385
+ with pytest.raises(ValueError, match="same number of values"):
1386
+ _run_selectors(
1387
+ tmp_path,
1388
+ monkeypatch,
1389
+ markers,
1390
+ ["sub-01", ("sub-02", "rest")],
1391
+ computed,
1392
+ )
1393
+ assert computed == []
1394
+
1395
+
1396
+ def _run_selectors(
1397
+ tmp_path: Path,
1398
+ monkeypatch: pytest.MonkeyPatch,
1399
+ markers: list[dict[str, str]],
1400
+ elements: Elements,
1401
+ computed: list[tuple[str, str]],
1402
+ ) -> int:
1403
+ """Run with element selectors, only recording the elements to compute.
1404
+
1405
+ The dataset has two subjects (``sub-01`` and ``sub-02``) with two tasks
1406
+ (``rest`` and ``movie``).
1407
+
1408
+ Parameters
1409
+ ----------
1410
+ tmp_path : pathlib.Path
1411
+ The path to the test directory.
1412
+ monkeypatch : pytest.MonkeyPatch
1413
+ The pytest.MonkeyPatch object.
1414
+ markers : list of dict
1415
+ Testing markers as list of dictionary.
1416
+ elements : list of str or tuple
1417
+ The element selectors.
1418
+ computed : list of tuple of str
1419
+ The list to add the computed elements to.
1420
+
1421
+ Returns
1422
+ -------
1423
+ int
1424
+ The number of searches of the elements of the dataset.
1425
+
1426
+ """
1427
+ datadir = tmp_path / "data"
1428
+ for subject in ("sub-01", "sub-02"):
1429
+ (datadir / subject).mkdir(parents=True)
1430
+ for task in ("rest", "movie"):
1431
+ (datadir / subject / f"{subject}_task-{task}_bold.nii").touch()
1432
+ n_searches = 0
1433
+ get_elements = PatternDataGrabber.get_elements
1434
+
1435
+ def fit(self: MarkerCollection, input: dict) -> None:
1436
+ element = input["BOLD"]["meta"]["element"]
1437
+ computed.append((element["subject"], element["task"]))
1438
+
1439
+ def counted_get_elements(self: PatternDataGrabber) -> Elements:
1440
+ nonlocal n_searches
1441
+ n_searches += 1
1442
+ return get_elements(self)
1443
+
1444
+ monkeypatch.setattr(MarkerCollection, "fit", fit)
1445
+ monkeypatch.setattr(
1446
+ PatternDataGrabber, "get_elements", counted_get_elements
1447
+ )
1448
+ run(
1449
+ workdir=tmp_path / "workdir",
1450
+ datagrabber={
1451
+ "kind": "PatternDataGrabber",
1452
+ "datadir": str(datadir),
1453
+ "types": ["BOLD"],
1454
+ "patterns": {
1455
+ "BOLD": {
1456
+ "pattern": "{subject}/{subject}_task-{task}_bold.nii",
1457
+ "space": "MNI152NLin6Asym",
1458
+ },
1459
+ },
1460
+ "replacements": ["subject", "task"],
1461
+ },
1462
+ markers=markers,
1463
+ storage={
1464
+ "kind": "HDF5FeatureStorage",
1465
+ "uri": str(tmp_path / "out.hdf5"),
1466
+ },
1467
+ elements=elements,
1468
+ )
1469
+ return n_searches
1470
+
1471
+
1472
+ @register_marker
1473
+ class _ImageMean(BaseMarker):
1474
+ """Marker storing the mean of an image, for BOLD and VBM_GM."""
1475
+
1476
+ _DEPENDENCIES: ClassVar[Dependencies] = {"numpy"}
1477
+ _MARKER_INOUT_MAPPINGS: ClassVar[MarkerInOutMappings] = {
1478
+ DataType.BOLD: {"mean": StorageType.Vector},
1479
+ DataType.VBM_GM: {"mean": StorageType.Vector},
1480
+ }
1481
+
1482
+ def compute(self, input: dict, extra_input: dict | None = None) -> dict:
1483
+ data = np.asarray(input["data"].dataobj, dtype=float)
1484
+ return {
1485
+ "mean": {"data": np.array([[data.mean()]]), "col_names": ["mean"]}
1486
+ }
1487
+
1488
+
1489
+ @pytest.mark.parametrize("single_output", [True, False])
1490
+ @pytest.mark.parametrize(
1491
+ "storage_kind", ["HDF5FeatureStorage", "SQLiteFeatureStorage"]
1492
+ )
1493
+ def test_run_data_types_element_keys(
1494
+ tmp_path: Path, storage_kind: str, single_output: bool
1495
+ ) -> None:
1496
+ """Test run function storing data types with different element keys.
1497
+
1498
+ The BOLD data depends on the subject and the task, and the VBM data only
1499
+ on the subject, so it is stored once for each subject. The elements are
1500
+ run twice, as when trying a few elements and then running all of them.
1501
+
1502
+ Parameters
1503
+ ----------
1504
+ tmp_path : pathlib.Path
1505
+ The path to the test directory.
1506
+ storage_kind : str
1507
+ The parametrized storage.
1508
+ single_output : bool
1509
+ The parametrized storage in a single file.
1510
+
1511
+ """
1512
+ datadir = tmp_path / "data"
1513
+ # BOLD: 10 * subject + task (1: rest, 2: movie); VBM: subject
1514
+ for i, subject in enumerate(("sub-01", "sub-02"), start=1):
1515
+ (datadir / subject).mkdir(parents=True)
1516
+ nib.save(
1517
+ nib.Nifti1Image(np.full((2, 2, 2), float(i)), np.eye(4)),
1518
+ datadir / subject / f"{subject}_GM.nii",
1519
+ )
1520
+ for j, task in enumerate(("rest", "movie"), start=1):
1521
+ nib.save(
1522
+ nib.Nifti1Image(
1523
+ np.full((2, 2, 2, 3), 10.0 * i + j), np.eye(4)
1524
+ ),
1525
+ datadir / subject / f"{subject}_task-{task}_bold.nii",
1526
+ )
1527
+ datagrabber = {
1528
+ "kind": "PatternDataGrabber",
1529
+ "datadir": str(datadir),
1530
+ "types": ["BOLD", "VBM_GM"],
1531
+ "patterns": {
1532
+ "BOLD": {
1533
+ "pattern": "{subject}/{subject}_task-{task}_bold.nii",
1534
+ "space": "native",
1535
+ },
1536
+ "VBM_GM": {
1537
+ "pattern": "{subject}/{subject}_GM.nii",
1538
+ "space": "native",
1539
+ },
1540
+ },
1541
+ "replacements": ["subject", "task"],
1542
+ }
1543
+ uri = tmp_path / "out" / "features.db"
1544
+ storage = {
1545
+ "kind": storage_kind,
1546
+ "uri": str(uri),
1547
+ "single_output": single_output,
1548
+ }
1549
+ # A few elements, and then all of them
1550
+ for elements in ([("sub-01", "rest")], None):
1551
+ run(
1552
+ workdir=tmp_path / "workdir",
1553
+ datagrabber=deepcopy(datagrabber),
1554
+ markers=[{"kind": "_ImageMean", "name": "mean"}],
1555
+ storage=deepcopy(storage),
1556
+ elements=elements,
1557
+ )
1558
+ if not single_output:
1559
+ collect(deepcopy(storage))
1560
+
1561
+ reader = PipelineComponentRegistry().build_component_instance(
1562
+ step="storage",
1563
+ name=storage_kind,
1564
+ baseclass=BaseFeatureStorage,
1565
+ init_params={"uri": uri},
1566
+ )
1567
+ features = {
1568
+ meta["name"]: md5 for md5, meta in reader.list_features().items()
1569
+ }
1570
+ bold = reader.read_df(feature_md5=features["BOLD_mean_mean"])
1571
+ vbm = reader.read_df(feature_md5=features["VBM_GM_mean_mean"])
1572
+ assert sorted(bold.index) == [
1573
+ ("sub-01", "movie"),
1574
+ ("sub-01", "rest"),
1575
+ ("sub-02", "movie"),
1576
+ ("sub-02", "rest"),
1577
+ ]
1578
+ assert sorted(bold["mean"]) == [11, 12, 21, 22]
1579
+ # Once for each subject
1580
+ assert vbm.index.names == ["subject"]
1581
+ assert sorted(vbm.index.get_level_values("subject")) == [
1582
+ "sub-01",
1583
+ "sub-02",
1584
+ ]
1585
+ assert sorted(vbm["mean"]) == [1, 2]