Repository: bids-standard/pybids

Issue:
Warn or error on misspecified (or changed due to pybids changes) target value
Going through the tutorial I saw that 
```python
# See all modality labels in this dataset
layout.get(return_type='id', target='modality')
```
is no longer producing any output (well - returns an empty list) although before was reporting `['anat', 'fmap', 'func']`.  Even though I know now -- CHANGELOG helps, it is `datatype` which is actually nothing -- I think it is an overall usability aspect. If I am asking to return some specific target, I can assume that there must be one defined/known, and then if nothing is actually returned, I would have appreciated to list what are the available "targets" here.  So, by default, I would have even appreciated an exception like `TargetNotFound` which ideally would have a list of available targets in its message or str; and may be optionally have a mode to just skip unknown targets. Not sure if that is something to be done on pybids or grabbit level, and not sure if I am missing some "flexibility" issue here that targets could be some arbitrary thing?
Warn or error on misspecified (or changed due to pybids changes) target value
Going through the tutorial I saw that 
```python
# See all modality labels in this dataset
layout.get(return_type='id', target='modality')
```
is no longer producing any output (well - returns an empty list) although before was reporting `['anat', 'fmap', 'func']`.  Even though I know now -- CHANGELOG helps, it is `datatype` which is actually nothing -- I think it is an overall usability aspect. If I am asking to return some specific target, I can assume that there must be one defined/known, and then if nothing is actually returned, I would have appreciated to list what are the available "targets" here.  So, by default, I would have even appreciated an exception like `TargetNotFound` which ideally would have a list of available targets in its message or str; and may be optionally have a mode to just skip unknown targets. Not sure if that is something to be done on pybids or grabbit level, and not sure if I am missing some "flexibility" issue here that targets could be some arbitrary thing?


Discussion:
I'd support raising an exception if the `target` is invalid. Probably without displaying a list of available targets though, as that's already pretty long. Maybe instead it can just say that `target` must be the name of a define BIDS keyword like subject, run, etc.
What about some fancy matching? e.g. like git or datalad does it? ;)
```shell
$> datalad insta                                                                 
datalad: Unknown command 'insta'.  See 'datalad --help'.
 
Did you mean one of these?
        install
        uninstall
```
The funny part is that it is one of those "batteries included" cases where actual code to get those is 1 line, smth like
```python
        if target not in known_targets:
            import difflib
            suggestions = difflib.get_close_matches(target, known_targets)
```
;-)
Although I would have still preferred a full sorted list.  Would really help guiding users.
That sounds good to me!
hm... tricky thing is that both bids and derivatives could be considered and may be `target` is valid/present in derivatives so failure shouldn't be thrown if grabbit level **but** actual selection is happening a the grabbit level. ok, will try to do ;-)
I think this should definitely happen at the pybids level... there's a `get()` wrapper around grabbit (which is where the handling of derivatives happens), so it could go there. I'm already having to consolidate all valid entities in one place anyway, so you can probably just abstract the chunk of code [here](https://github.com/bids-standard/pybids/blob/master/bids/layout/layout.py#L329) (L329 - L333) and call that. :)
Oh, duh, it's even easier than that--just compare `target` with `ent_kwargs`--this is exactly where this logic should go ;)
I feel like you are already on top of it! ;)
I'd support raising an exception if the `target` is invalid. Probably without displaying a list of available targets though, as that's already pretty long. Maybe instead it can just say that `target` must be the name of a define BIDS keyword like subject, run, etc.
What about some fancy matching? e.g. like git or datalad does it? ;)
```shell
$> datalad insta                                                                 
datalad: Unknown command 'insta'.  See 'datalad --help'.
 
Did you mean one of these?
        install
        uninstall
```
The funny part is that it is one of those "batteries included" cases where actual code to get those is 1 line, smth like
```python
        if target not in known_targets:
            import difflib
            suggestions = difflib.get_close_matches(target, known_targets)
```
;-)
Although I would have still preferred a full sorted list.  Would really help guiding users.
That sounds good to me!
hm... tricky thing is that both bids and derivatives could be considered and may be `target` is valid/present in derivatives so failure shouldn't be thrown if grabbit level **but** actual selection is happening a the grabbit level. ok, will try to do ;-)
I think this should definitely happen at the pybids level... there's a `get()` wrapper around grabbit (which is where the handling of derivatives happens), so it could go there. I'm already having to consolidate all valid entities in one place anyway, so you can probably just abstract the chunk of code [here](https://github.com/bids-standard/pybids/blob/master/bids/layout/layout.py#L329) (L329 - L333) and call that. :)
Oh, duh, it's even easier than that--just compare `target` with `ent_kwargs`--this is exactly where this logic should go ;)
I feel like you are already on top of it! ;)

Resolving patch:
diff --git a/bids/layout/layout.py b/bids/layout/layout.py
--- a/bids/layout/layout.py
+++ b/bids/layout/layout.py
@@ -397,6 +397,18 @@ def get(self, return_type='object', target=None, extensions=None,
             else:
                 md_kwargs[k] = v
 
+        # Provide some suggestions if target is specified and invalid.
+        if target is not None and target not in all_ents:
+            import difflib
+            potential = list(all_ents.keys())
+            suggestions = difflib.get_close_matches(target, potential)
+            if suggestions:
+                message = "Did you mean one of: {}?".format(suggestions)
+            else:
+                message = "Valid targets are: {}".format(potential)
+            raise ValueError(("Unknown target '{}'. " + message)
+                             .format(target))
+
         all_results = []
 
         # Get entity-based search results using the superclass's get()


Test patch:
diff --git a/bids/layout/tests/test_layout.py b/bids/layout/tests/test_layout.py
--- a/bids/layout/tests/test_layout.py
+++ b/bids/layout/tests/test_layout.py
@@ -135,6 +135,17 @@ def test_get_metadata_via_bidsfile(layout_7t_trt):
     assert 'subject' not in result
 
 
+def test_get_with_bad_target(layout_7t_trt):
+    with pytest.raises(ValueError) as exc:
+        layout_7t_trt.get(target='unicorn')
+        msg = exc.value.message
+        assert 'subject' in msg and 'reconstruction' in msg and 'proc' in msg
+    with pytest.raises(ValueError) as exc:
+        layout_7t_trt.get(target='sub')
+        msg = exc.value.message
+        assert 'subject' in msg and 'reconstruction' not in msg
+
+
 def test_get_bvals_bvecs(layout_ds005):
     dwifile = layout_ds005.get(subject="01", datatype="dwi")[0]
     result = layout_ds005.get_bval(dwifile.path)