Repository: nipreps/smriprep

Issue:
`collect_derivatives` modifies queries in place
**Describe the bug**
The `collect_derivatives` function is modifying its queries as it loops over spaces, with the first space listed being retained in later spaces' queries. For example, if I search for transforms with spaces ['a', 'b', 'c'], all three spaces' transform queries will use 'a'.

Stems from https://github.com/nipreps/fmriprep/pull/3491#issuecomment-3164693271.

Here is a script to reproduce the error. Note that the transforms in the output dictionary for fsnative and MNI152NLin6Asym spaces have `'from'/'to': 'MNI152NLin2009cAsym'`.

```python
import os
from pprint import pprint

from bids.layout import BIDSLayout
from niworkflows.utils.testing import generate_bids_skeleton
from smriprep.utils.bids import collect_derivatives

# Dictionary representation of dataset
skeleton_dict = {
    '01': [
        {
            'anat': [
                {
                    'desc': 'preproc',
                    'suffix': 'T1w'
                },
                {
                    'from': 'fsnative',
                    'to': 'T1w',
                    'mode': 'image',
                    'suffix': 'xfm',
                    'extension': '.txt'
                },
                {
                    'from': 'T1w',
                    'to': 'fsnative',
                    'mode': 'image',
                    'suffix': 'xfm',
                    'extension': '.txt'
                },
                {
                    'from': 'MNI152NLin2009cAsym',
                    'to': 'T1w',
                    'mode': 'image',
                    'suffix': 'xfm',
                    'extension': '.h5'
                },
                {
                    'from': 'T1w',
                    'to': 'MNI152NLin2009cAsym',
                    'mode': 'image',
                    'suffix': 'xfm',
                    'extension': '.h5'
                },
                {
                    'from': 'MNI152NLin6Asym',
                    'to': 'T1w',
                    'mode': 'image',
                    'suffix': 'xfm',
                    'extension': '.h5'
                },
                {
                    'from': 'T1w',
                    'to': 'MNI152NLin6Asym',
                    'mode': 'image',
                    'suffix': 'xfm',
                    'extension': '.h5'
                }
            ]
        }
    ]
}

bids_dir = os.path.abspath('smriprep_test_dset')
generate_bids_skeleton(str(bids_dir), skeleton_dict)
layout = BIDSLayout(str(bids_dir))

precomputed = collect_derivatives(
    bids_dir,
    '01',
    ['MNI152NLin2009cAsym', 'fsnative', 'MNI152NLin6Asym'],
    spec=None,
    patterns=None,
    session_id=None,
)
pprint(precomputed)

"""
{'t1w_preproc': '.../sub-01_desc-preproc_T1w.nii.gz',
 'transforms': {'MNI152NLin2009cAsym': {'forward': '.../sub-01_from-T1w_to-MNI152NLin2009cAsym_mode-image_xfm.h5',
                                        'reverse': '.../sub-01_from-MNI152NLin2009cAsym_to-T1w_mode-image_xfm.h5'},
                'MNI152NLin6Asym': {'forward': '.../sub-01_from-T1w_to-MNI152NLin2009cAsym_mode-image_xfm.h5',
                                    'reverse': '.../sub-01_from-MNI152NLin2009cAsym_to-T1w_mode-image_xfm.h5'},
                'fsnative': {'forward': '.../sub-01_from-T1w_to-MNI152NLin2009cAsym_mode-image_xfm.h5',
                             'reverse': '.../sub-01_from-MNI152NLin2009cAsym_to-T1w_mode-image_xfm.h5'}}}
"""
```

**Are you positive that the input dataset is BIDS-compliant?**
- [ ] I have used the online BIDS-Validator
- [ ] I have run a local installation of the BIDS-Validator (please, note the version of the validator here).
- [ ] I let *sMRIPrep* check it for me (in other words, **I didn't set the `--skip-bids-validation` argument**).
- [x] No, I haven't checked myself AND used the `--skip-bids-validation` argument.


Discussion:

Fix (patch):
diff --git a/src/smriprep/utils/bids.py b/src/smriprep/utils/bids.py
index cf6790f..4633913 100644
--- a/src/smriprep/utils/bids.py
+++ b/src/smriprep/utils/bids.py
@@ -59,7 +59,7 @@ def collect_derivatives(
         qry_base['session'] = session_id
 
     for key, qry in spec['baseline'].items():
-        qry |= qry_base
+        qry = {**qry, **qry_base}
         item = layout.get(**qry)
         if not item:
             continue
@@ -76,7 +76,7 @@ def collect_derivatives(
     for _space in std_spaces:
         space = _space.replace(':cohort-', '+')
         for key, qry in spec['transforms'].items():
-            qry |= qry_base
+            qry = {**qry, **qry_base}
             qry['from'] = qry['from'] or space
             qry['to'] = qry['to'] or space
             item = layout.get(return_type='filename', **qry)
@@ -85,7 +85,7 @@ def collect_derivatives(
             transforms.setdefault(_space, {})[key] = item[0] if len(item) == 1 else item
 
     for key, qry in spec['surfaces'].items():
-        qry |= qry_base
+        qry = {**qry, **qry_base}
         item = layout.get(return_type='filename', **qry)
         if not item or len(item) != 2:
             continue


Test changes:
diff --git a/src/smriprep/utils/tests/test_bids.py b/src/smriprep/utils/tests/test_bids.py
index a8b094e..6866ea7 100644
--- a/src/smriprep/utils/tests/test_bids.py
+++ b/src/smriprep/utils/tests/test_bids.py
@@ -1,14 +1,20 @@
+import pytest
 from niworkflows.utils.testing import generate_bids_skeleton
 
 from ..bids import collect_derivatives
 from . import DERIV_SKELETON
 
 
-def test_collect_derivatives(tmp_path):
+@pytest.fixture
+def deriv_dset(tmp_path):
     deriv_dir = tmp_path / 'derivatives'
     generate_bids_skeleton(deriv_dir, str(DERIV_SKELETON))
+    return deriv_dir
+
+
+def test_collect_derivatives(deriv_dset):
     output_spaces = ['MNI152NLin2009cAsym', 'MNIPediatricAsym:cohort-3']
-    collected = collect_derivatives(deriv_dir, '01', output_spaces)
+    collected = collect_derivatives(deriv_dset, '01', output_spaces)
     for suffix in ('preproc', 'mask', 'dseg'):
         assert collected[f't1w_{suffix}']
     assert len(collected['t1w_tpms']) == 3
@@ -28,3 +34,14 @@ def test_collect_derivatives(tmp_path):
         'sphere_reg_msm',
     ):
         assert len(collected[surface]) == 2
+
+
+def test_collect_derivatives_transforms(deriv_dset):
+    """Ensure transforms are collected for the right spaces."""
+    output_spaces = ['MNI152NLin2009cAsym', 'MNIPediatricAsym:cohort-3']
+    collected = collect_derivatives(deriv_dset, '01', output_spaces)
+    xfms = collected['transforms']
+    for space in output_spaces:
+        template = space.split(':')[0]
+        assert template in xfms[space]['reverse']
+        assert template in xfms[space]['forward']