Repository: bids-standard/pybids
Pull request #580 (2020-01-31 00:10:49)

FIX: BIDSLayout.build_path to respect absolute_paths.
This PR makes BIDSLayout.build_path return an absolute path if BIDSLayout.absolute_paths is True.

I'm not sure if this is desired behaviour - but I expected the following:

```python
bids_layout = bids.BIDSLayout('/path/to/root', absolute_paths=True)
bids_layout.build_path(entities)
# /path/to/root/sub-../ses-../...nii.gz
```


Patch:
diff --git a/bids/layout/layout.py b/bids/layout/layout.py
index 6b255c66..dcda36e1 100644
--- a/bids/layout/layout.py
+++ b/bids/layout/layout.py
@@ -1404,7 +1404,7 @@ class BIDSLayout(object):
         return all_trs.pop()
 
     def build_path(self, source, path_patterns=None, strict=False,
-                   scope='all', validate=True):
+                   scope='all', validate=True, absolute_paths=None):
         """Construct a target filename for a file or dictionary of entities.
 
         Parameters
@@ -1446,6 +1446,11 @@ class BIDSLayout(object):
             If True, built path must pass BIDS validator. If
             False, no validation is attempted, and an invalid path may be
             returned (e.g., if an entity value contains a hyphen).
+        absolute_paths : bool, optional
+            Optionally override the instance-wide option
+            to report either absolute or relative (to the top of the
+            dataset) paths. If None, will fall back on the value specified
+            at BIDSLayout initialization.
         """
         # 'is_file' is a crude check for Path objects
         if isinstance(source, str) or hasattr(source, 'is_file'):
@@ -1476,12 +1481,19 @@ class BIDSLayout(object):
                 "Unable to construct build path with source {}".format(source))
         to_check = os.path.join(os.path.sep, built)
 
-        if not validate or BIDSValidator().is_bids(to_check):
-            return built
+        if validate and not BIDSValidator().is_bids(to_check):
+            raise ValueError("Built path {} is not a valid BIDS filename. "
+                             "Please make sure all provided entity values are "
+                             "spec-compliant.".format(built))
+
+        # Convert to absolute paths if needed
+        if absolute_paths is None:
+            absolute_paths = self.absolute_paths
+
+        if absolute_paths:
+            built = os.path.join(self.root, built)
 
-        raise ValueError("Built path {} is not a valid BIDS filename. Please "
-                         "make sure all provided entity values are "
-                         "spec-compliant.".format(built))
+        return built
 
     def copy_files(self, files=None, path_patterns=None, symbolic_links=True,
                    root=None, conflicts='fail', **kwargs):


Test patch:
diff --git a/bids/layout/tests/test_path_building.py b/bids/layout/tests/test_path_building.py
index e137c5d7..560a70d4 100644
--- a/bids/layout/tests/test_path_building.py
+++ b/bids/layout/tests/test_path_building.py
@@ -1,6 +1,7 @@
 import pytest
 from bids.layout import BIDSLayout
 from os.path import join, abspath, sep
+from pathlib import Path
 from bids.tests import get_test_data_path
 
 
@@ -12,9 +13,12 @@ def layout():
 
 def test_bold_construction(layout):
     ents = dict(subject='01', run=1, task='rest', suffix='bold')
-    assert layout.build_path(ents) == "sub-01/func/sub-01_task-rest_run-1_bold.nii.gz"
-    ents['acquisition'] = 'random'
-    assert layout.build_path(ents) == "sub-01/func/sub-01_task-rest_acq-random_run-1_bold.nii.gz"
+    relative = Path("sub-01") / "func" / "sub-01_task-rest_run-1_bold.nii.gz"
+    absolute = Path(layout.root) / relative
+    assert layout.build_path(ents, absolute_paths=False) == str(relative)
+    assert layout.build_path(ents, absolute_paths=True) == str(absolute)
+    # layout fixture created with `absolute_paths=True`, defaulting to absolute
+    assert layout.build_path(ents) == str(absolute)
 
 
 def test_invalid_file_construction(layout):
@@ -24,7 +28,7 @@ def test_invalid_file_construction(layout):
         layout.build_path(ents)
 
     target = "sub-01/func/sub-01_task-resting-state_run-1_bold.nii.gz"
-    assert layout.build_path(ents, validate=False) == target
+    assert layout.build_path(ents, validate=False, absolute_paths=False) == target
 
 
 def test_failed_file_construction(layout):