diff --git a/bids/layout/tests/test_utils.py b/bids/layout/tests/test_utils.py index 1023fb769..dda88adc9 100644 --- a/bids/layout/tests/test_utils.py +++ b/bids/layout/tests/test_utils.py @@ -3,11 +3,22 @@ import os import pytest + +from pathlib import Path + import bids + from bids.exceptions import ConfigError from ..models import Entity, Config + from ..utils import BIDSMetadata, parse_file_entities, add_config_paths +from ..utils import write_description +from ..utils import get_description_fields + +from ...tests import get_test_data_path + + def test_bidsmetadata_class(): @@ -60,3 +71,58 @@ def test_add_config_paths(): add_config_paths(dummy=bids_json) config = Config.load('dummy') assert 'subject' in config.entities + +# teardown +# @pytest.fixture() +# def clean_up(output_dir): +# yield +# os.remove(output_dir) + +def test_write_description_raw(exist_ok=True): + + write_description(name="test", is_derivative=False, exist_ok=exist_ok) + + output_dir = Path().resolve(); + bids.BIDSLayout(Path.joinpath(output_dir, 'raw')) + + # teardown + os.remove(Path.joinpath(output_dir, 'raw', 'dataset_description.json')) + +def test_write_description_derivatives(exist_ok=True): + + source_dir = get_test_data_path("Path") / '7t_trt' + + write_description(source_dir=source_dir, name="test", + exist_ok=exist_ok) + + bids.BIDSLayout(source_dir, derivatives=True) + + # teardown + os.remove(Path.joinpath(source_dir, 'derivatives', 'test', 'dataset_description.json')) + +def test_write_description_derivatives_outside_raw(exist_ok=True): + + source_dir = get_test_data_path("Path") / '7t_trt' + output_dir = Path().resolve(); + + write_description(source_dir=source_dir, name="test", + output_dir=output_dir, + exist_ok=exist_ok) + + bids.BIDSLayout(Path.joinpath(output_dir, 'derivatives', 'test')) + + # teardown + os.remove(Path.joinpath(output_dir, 'derivatives', 'test', 'dataset_description.json')) + + +def test_get_description_fields(): + + fields = get_description_fields("1.1.1", "required") + assert fields == ["Name", "BIDSVersion"] + + fields = get_description_fields("1.6.1", "required") + assert fields == ["Name", "BIDSVersion", "GeneratedBy"] + +def test_get_description_fields_error(): + + pytest.raises(TypeError, get_description_fields, 1, "required") \ No newline at end of file diff --git a/bids/layout/utils.py b/bids/layout/utils.py index 5d30d094c..1ca5f5bd5 100644 --- a/bids/layout/utils.py +++ b/bids/layout/utils.py @@ -1,10 +1,13 @@ """Miscellaneous layout-related utilities.""" +import os +import json + from pathlib import Path from .. import config as cf from ..utils import make_bidsfile, listify from ..exceptions import ConfigError - +from packaging.version import Version class BIDSMetadata(dict): """ Metadata dictionary that reports the associated file on lookup failures. """ @@ -97,3 +100,116 @@ def add_config_paths(**kwargs): kwargs.update(**cf.get_option('config_paths')) cf.set_option('config_paths', kwargs) + + +desc_fields = { + Version("1.1.1"): { + "required": ["Name", "BIDSVersion"], + "recommended": ["License"], + "optional": ["Authors", "Acknowledgements", "HowToAcknowledge", + "Funding", "ReferencesAndLinks", "DatasetDOI"] + }, + # TODO make more general + # assumes we only want to generate with derivatives + # deal with the different requirements for raw and derivative + Version("1.6.1"): { + "required": ["Name", "BIDSVersion", "GeneratedBy"], + "recommended": ["License", "DatasetType", "HEDVersion"], + "optional": ["Authors", "Acknowledgements", "HowToAcknowledge", + "Funding", "ReferencesAndLinks", "DatasetDOI", + "EthicsApprovals","SourceDatasets"] + } +} + +def get_description_fields(version:str, type_:str): + if isinstance(version, str): + version = Version(version) + if not isinstance(version, Version): + raise TypeError("Version must be a string or a packaging.version.Version object.") + + if version in desc_fields: + return desc_fields[version][type_] + return desc_fields[max(desc_fields.keys())][type_] + + +def write_description(output_dir="", bids_version='1.6.1', name="", + is_derivative=True, + source_dir="", pipeline_version="0.1.0", + exist_ok=False, propagate=False, **desc_kwargs): + """Write a dataset_description.json file for a new derivative folder. + + Parameters + ---------- + source_dir : str or Path + Directory of the BIDS dataset that has been derived. + This dataset can itself be a derivative. + name : str + Name of the derivative dataset. + bids_version: str + Version of the BIDS standard. + exist_ok : bool + Control the behavior of pathlib.Path.mkdir when a derivative folder + with this name already exists. + propagate: bool + If set to True (default to False), fields that are not explicitly + provided in desc_kwargs get propagated to the derivatives. Else, + these fields get no values. + desc_kwargs: dict + Dictionary of entries that should be added to the + dataset_description.json file. + """ + + if not is_derivative: + + desc = { + 'Name': name, + 'BIDSVersion': bids_version, + } + + if output_dir == "": + output_dir = Path.joinpath(Path().resolve(), 'raw') + + else: + + desc = { + 'Name': name, + 'BIDSVersion': bids_version, + 'GeneratedBy': [{ + "Name": name, + "Version": pipeline_version + }], + "SourceDatasets": "" + } + + if source_dir == "": + raise ("Provide a source dataset for your derivatives.") + + # we let user decide where to output the derivatives + if output_dir == "": + output_dir = source_dir + + output_dir = Path.joinpath(output_dir, "derivatives", name) + + fname = source_dir / 'dataset_description.json' + if not fname.exists(): + raise ValueError("The argument source_dir must point to a valid BIDS directory." + + "As such, it should contain a dataset_description.json file.") + orig_desc = json.loads(fname.read_text()) + + if propagate: + for field_type in ["recommended", "optional"]: + for field in get_description_fields(bids_version, field_type): + if field in desc: + continue + if field in orig_desc: + desc[field] = orig_desc[field] + + desc.update(desc_kwargs) + + for field in get_description_fields(bids_version, "required"): + if field not in desc: + raise ValueError("The field {} is required and is currently missing.".format(field)) + + Path.joinpath + output_dir.mkdir(parents=True, exist_ok=exist_ok) + Path.write_text(output_dir / 'dataset_description.json', json.dumps(desc, indent=4)) diff --git a/bids/tests/utils.py b/bids/tests/utils.py index d0dca928e..10f84b079 100644 --- a/bids/tests/utils.py +++ b/bids/tests/utils.py @@ -1,7 +1,25 @@ ''' Test-related utilities ''' +from pathlib import Path +import pkg_resources -from os.path import join, dirname, abspath +def get_test_data_path(return_type="str"): + """ + Parameters + ---------- + return_type : str or Path + Specify the type of object returned. Can be 'str' + (default, for backward-compatibility) or 'Path' for pathlib.Path type. -def get_test_data_path(): - return join(dirname(abspath(__file__)), 'data') + Returns + ------- + The path for testing data. + """ + + path = pkg_resources.resource_filename('bids', 'tests/data') + if return_type == "str": + return path + elif return_type == "Path": + return Path(path) + else: + raise ValueError("return_type can be 'str' or 'Path. Got {}.".format(return_type)) diff --git a/setup.cfg b/setup.cfg index 15a6bfbdb..4ccb32f39 100644 --- a/setup.cfg +++ b/setup.cfg @@ -30,6 +30,7 @@ install_requires = sqlalchemy <1.4.0.dev0 bids-validator num2words + packaging click tests_require = pytest >=3.3