Repository: bids-standard/pybids

Issue:
Dynamic getters & entities from derivative layouts are not propagated to main Layout
Let's say I load a dataset with an fmriprep derivative in `derivatives/`

```
layout = BIDSLayout('/my/dataset', derivatives=True)
```
`layout.entities` will only list `bids.` entities, not any from `derivatives`

so in turn `layout.get_spaces()` will not work
but: `layout.derivatives['fMRIPrep'].get_spaces()` will.

Is this a problem? Seems a bit un-intuitive, and may have other unforeseen consequences if the top-level API is not aware of sub-Layout's entities.

Related issue: Should probably add a `fmriprep` derivative to the `ds005` test dataset.


Discussion:

Patch:
diff --git a/bids/layout/layout.py b/bids/layout/layout.py
index bdf9b99e..0861258c 100644
--- a/bids/layout/layout.py
+++ b/bids/layout/layout.py
@@ -4,11 +4,11 @@ import warnings
 from io import open
 from .validation import BIDSValidator
 from grabbit import Layout, File
-from grabbit.external import six
+from grabbit.external import six, inflect
 from grabbit.utils import listify
 import nibabel as nb
 from collections import defaultdict
-from functools import reduce
+from functools import reduce, partial
 from itertools import chain
 import re
 
@@ -194,6 +194,7 @@ class BIDSLayout(Layout):
                     if check_for_description(sd):
                         deriv_dirs.append(sd)
 
+        local_entities = set(ent.name for ent in self.entities.values())
         for deriv in deriv_dirs:
             dd = os.path.join(deriv, 'dataset_description.json')
             with open(dd, 'r', encoding='utf-8') as ddfd:
@@ -213,6 +214,16 @@ class BIDSLayout(Layout):
             kwargs['sources'] = kwargs.get('sources') or self
             self.derivatives[pipeline_name] = BIDSLayout(deriv, **kwargs)
 
+            # Propagate derivative entities into top-level dynamic getters
+            deriv_entities = set(ent.name
+                                 for ent in self.derivatives[pipeline_name].entities.values())
+            for deriv_ent in deriv_entities - local_entities:
+                local_entities.add(deriv_ent)
+                getter = 'get_' + inflect.engine().plural(deriv_ent)
+                if not hasattr(self, getter):
+                    func = partial(self.get, target=deriv_ent, return_type='id')
+                    setattr(self, getter, func)
+
     def to_df(self, **kwargs):
         """
         Return information for all Files tracked in the Layout as a pandas


Test patch:
diff --git a/bids/layout/tests/test_layout.py b/bids/layout/tests/test_layout.py
index 18fb2d0f..f6ccc122 100644
--- a/bids/layout/tests/test_layout.py
+++ b/bids/layout/tests/test_layout.py
@@ -255,3 +255,12 @@ def test_restricted_words_in_path(tmpdir):
     orig_files = set(f.replace(orig_path, '') for f in orig_layout.files)
     new_files = set(f.replace(new_path, '') for f in new_layout.files)
     assert orig_files == new_files
+
+
+def test_derivative_getters():
+    synth_path = join(get_test_data_path(), 'synthetic')
+    bare_layout = BIDSLayout(synth_path, derivatives=False)
+    full_layout = BIDSLayout(synth_path, derivatives=True)
+    with pytest.raises(AttributeError):
+        bare_layout.get_spaces()
+    assert set(full_layout.get_spaces()) == {'MNI152NLin2009cAsym', 'T1w'}