diff --git a/bids/layout/layout.py b/bids/layout/layout.py index 44b73c892..3bde42be0 100644 --- a/bids/layout/layout.py +++ b/bids/layout/layout.py @@ -14,7 +14,7 @@ import sqlalchemy as sa from bids_validator import BIDSValidator -from ..utils import listify, natural_sort +from ..utils import listify, natural_sort, hashablefy from ..external import inflect from ..exceptions import ( BIDSDerivativesValidationError, @@ -677,7 +677,7 @@ def get(self, return_type='object', target=None, scope='all', results = [x for x in results if target in x.entities] if return_type == 'id': - results = list(set([x.entities[target] for x in results])) + results = list(set([hashablefy(x.entities[target]) for x in results])) results = natural_sort(results) elif return_type == 'dir': diff --git a/bids/layout/tests/test_layout.py b/bids/layout/tests/test_layout.py index 02650dae5..ebc17fca1 100644 --- a/bids/layout/tests/test_layout.py +++ b/bids/layout/tests/test_layout.py @@ -457,6 +457,27 @@ def test_get_tr(layout_7t_trt): assert tr == 4.0 +def test_get_nonhashable_metadata(layout_ds117): + """Test nonhashable metadata values (#683).""" + assert layout_ds117.get_IntendedFor(subject=['01'])[0] == ( + "ses-mri/func/sub-01_ses-mri_task-facerecognition_run-01_bold.nii.gz", + "ses-mri/func/sub-01_ses-mri_task-facerecognition_run-02_bold.nii.gz", + "ses-mri/func/sub-01_ses-mri_task-facerecognition_run-03_bold.nii.gz", + "ses-mri/func/sub-01_ses-mri_task-facerecognition_run-04_bold.nii.gz", + "ses-mri/func/sub-01_ses-mri_task-facerecognition_run-05_bold.nii.gz", + "ses-mri/func/sub-01_ses-mri_task-facerecognition_run-06_bold.nii.gz", + "ses-mri/func/sub-01_ses-mri_task-facerecognition_run-07_bold.nii.gz", + "ses-mri/func/sub-01_ses-mri_task-facerecognition_run-08_bold.nii.gz", + "ses-mri/func/sub-01_ses-mri_task-facerecognition_run-09_bold.nii.gz", + ) + + landmarks = layout_ds117.get_AnatomicalLandmarkCoordinates(subject=['01'])[0] + assert landmarks["Nasion"] == (43, 111, 95) + assert landmarks["LPA"] == (140, 74, 16) + assert landmarks["RPA"] == (143, 74, 173) + + + def test_to_df(layout_ds117): # Only filename entities df = layout_ds117.to_df() diff --git a/bids/utils.py b/bids/utils.py index 100a3c194..b5b69b74e 100644 --- a/bids/utils.py +++ b/bids/utils.py @@ -2,6 +2,11 @@ import re import os +from frozendict import frozendict + + +# Monkeypatch to print out frozendicts *as if* they were dictionaries. +frozendict.__repr__ = lambda s: repr({k: v for k, v in s.items()}) def listify(obj): @@ -10,6 +15,16 @@ def listify(obj): return obj if isinstance(obj, (list, tuple, type(None))) else [obj] +def hashablefy(obj): + ''' Make dictionaries and lists hashable or raise. ''' + if isinstance(obj, list): + return tuple([hashablefy(o) for o in obj]) + + if isinstance(obj, dict): + return frozendict({k: hashablefy(v) for k, v in obj.items()}) + return obj + + def matches_entities(obj, entities, strict=False): ''' Checks whether an object's entities match the input. ''' if strict and set(obj.entities.keys()) != set(entities.keys()): diff --git a/setup.cfg b/setup.cfg index 450026d80..935b02675 100644 --- a/setup.cfg +++ b/setup.cfg @@ -31,6 +31,7 @@ install_requires = bids-validator num2words click + frozendict tests_require = pytest >=3.3 mock