Repository: bids-standard/pybids
Pull request #400 (2019-02-27 15:45:52)

ENH: respect absolute_path also for "dir" return_type
But what I wondered if `absolute_path` is really something to be defined at the Layout level, or should be just a (yet another) option to `.get` -- seems to neither (nor tested) anywhere besides in `.get()` and indeed it is more of an option on how to report paths/directories .  If not for  #397 I would have immediately added to `get` with default of `None` describing it as the way to overload the value defined at the class level (probably should be retained for compatibility for awhile). Let me know if I should do that @tyarkoni 



Patch:
diff --git a/bids/layout/core.py b/bids/layout/core.py
index 1081ef59..6dc7048c 100644
--- a/bids/layout/core.py
+++ b/bids/layout/core.py
@@ -275,7 +275,10 @@ class BIDSFile(object):
     def __getattr__(self, attr):
         # Ensures backwards compatibility with old File_ namedtuple, which is
         # deprecated as of 0.7.
-        if attr in self.entities:
+        # _ check first to not mask away access to __setstate__ etc.
+        # AFAIK None of the entities are allowed to start with _ anyways
+        # so the check is more generic than __
+        if not attr.startswith('_') and attr in self.entities:
             warnings.warn("Accessing entities as attributes is deprecated as "
                           "of 0.7. Please use the .entities dictionary instead"
                           " (i.e., .entities['%s'] instead of .%s."
diff --git a/bids/layout/layout.py b/bids/layout/layout.py
index 2f92515a..4843e729 100644
--- a/bids/layout/layout.py
+++ b/bids/layout/layout.py
@@ -110,9 +110,7 @@ class BIDSLayout(object):
         index_associated (bool): Argument passed onto the BIDSValidator;
             ignored if validate = False.
         absolute_paths (bool): If True, queries always return absolute paths.
-            If False, queries return relative paths, unless the root argument
-            was left empty (in which case the root defaults to the file system
-            root).
+            If False, queries return relative paths (for files and directories).
         derivatives (bool, str, list): Specifies whether and/or which
             derivatives to to index. If True, all pipelines found in the
             derivatives/ subdirectory will be indexed. If a str or list, gives
@@ -429,7 +427,9 @@ class BIDSLayout(object):
         return data
 
     def get(self, return_type='object', target=None, extensions=None,
-            scope='all', regex_search=False, defined_fields=None, **kwargs):
+            scope='all', regex_search=False, defined_fields=None,
+            absolute_paths=None,
+            **kwargs):
         """
         Retrieve files and/or metadata from the current Layout.
 
@@ -458,9 +458,13 @@ class BIDSLayout(object):
                 that must be defined in JSON sidecars in order to consider the
                 file a match, but which don't need to match any particular
                 value.
+            absolute_paths (bool): 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.
             kwargs (dict): Any optional key/values to filter the entities on.
                 Keys are entity names, values are regexes to filter on. For
-                example, passing filter={ 'subject': 'sub-[12]'} would return
+                example, passing filter={'subject': 'sub-[12]'} would return
                 only files that match the first two subjects.
 
         Returns:
@@ -520,7 +524,10 @@ class BIDSLayout(object):
                 results = [files[f] for f in results]
 
         # Convert to relative paths if needed
-        if not self.absolute_paths:
+        if absolute_paths is None:  # can be overloaded as option to .get
+            absolute_paths = self.absolute_paths
+
+        if not absolute_paths:
             for i, f in enumerate(results):
                 f = copy.copy(f)
                 f.path = os.path.relpath(f.path, self.root)
@@ -552,8 +559,12 @@ class BIDSLayout(object):
                     patt = entities[ent].pattern
                     template = template.replace('{%s}' % ent, patt)
                 template += r'[^\%s]*$' % os.path.sep
-                matches = [f.dirname for f in results
-                           if re.search(template, f.dirname)]
+                matches = [
+                    f.dirname if absolute_paths else os.path.relpath(f.dirname, self.root)
+                    for f in results
+                    if re.search(template, f.dirname)
+                ]
+
                 results = natural_sort(list(set(matches)))
 
             else:


Test patch:
diff --git a/bids/layout/tests/test_layout.py b/bids/layout/tests/test_layout.py
index b02e191a..acbcc239 100644
--- a/bids/layout/tests/test_layout.py
+++ b/bids/layout/tests/test_layout.py
@@ -18,6 +18,12 @@ def layout_7t_trt():
     return BIDSLayout(data_dir)
 
 
+@pytest.fixture(scope='module')
+def layout_7t_trt_relpath():
+    data_dir = join(get_test_data_path(), '7t_trt')
+    return BIDSLayout(data_dir, absolute_paths=False)
+
+
 @pytest.fixture(scope='module')
 def layout_ds005():
     data_dir = join(get_test_data_path(), 'ds005')
@@ -68,6 +74,21 @@ def test_layout_repr(layout_7t_trt):
     assert "Subjects: 10 | Sessions: 20 | Runs: 20" in str(layout_7t_trt)
 
 
+def test_layout_copy(layout_7t_trt):
+    # Largely a smoke test to guarantee that copy() does not blow
+    # see https://github.com/bids-standard/pybids/pull/400#issuecomment-467961124
+    import copy
+    l = layout_7t_trt
+
+    lcopy = copy.copy(l)
+    assert repr(lcopy) == repr(l)
+    assert str(lcopy) == str(l)
+
+    lcopy = copy.deepcopy(l)
+    assert repr(lcopy) == repr(l)
+    assert str(lcopy) == str(l)
+
+
 def test_load_description(layout_7t_trt):
     # Should not raise an error
     assert hasattr(layout_7t_trt, 'description')
@@ -230,16 +251,28 @@ def test_bids_json(layout_7t_trt):
     assert set(res) == {'1', '2'}
 
 
-def test_get_return_type_dir(layout_7t_trt):
-    l = layout_7t_trt
-    res = l.get(target='subject', return_type='dir')
+def test_get_return_type_dir(layout_7t_trt, layout_7t_trt_relpath):
+    query = dict(target='subject', return_type='dir')
+    # In case of relative paths
+    res_relpath = layout_7t_trt_relpath.get(**query)
     # returned directories should be in sorted order so we can match exactly
+    target_relpath = ["sub-{:02d}".format(i) for i in range(1, 11)]
+    assert target_relpath == res_relpath
+
+    res = layout_7t_trt.get(**query)
     target = [
-        os.path.join(get_test_data_path(), '7t_trt', "sub-{:02d}".format(i))
-        for i in range(1, 11)
+        os.path.join(get_test_data_path(), '7t_trt', p)
+        for p in target_relpath
     ]
     assert target == res
 
+    # and we can overload the value for absolute_path in .get call
+    res_relpath2 = layout_7t_trt.get(absolute_paths=False, **query)
+    assert target_relpath == res_relpath2
+    res2 = layout_7t_trt_relpath.get(absolute_paths=True, **query)
+    assert target == res2
+
+
 
 def test_get_return_sorted(layout_7t_trt):
     bids_files = layout_7t_trt.get(target='subject')