From de097eee9dd521288f4b8362b3271ebfbaf4a94e Mon Sep 17 00:00:00 2001 From: Christian O'Reilly Date: Fri, 22 Nov 2019 14:08:54 -0500 Subject: [PATCH 01/10] Adding write_derivative_description and the related test. --- bids/tests/utils.py | 29 +++++++++++-- bids/utils.py | 101 +++++++++++++++++++++++++++++++++++++++----- setup.py | 4 +- 3 files changed, 119 insertions(+), 15 deletions(-) diff --git a/bids/tests/utils.py b/bids/tests/utils.py index d0dca928e..5516d7e94 100644 --- a/bids/tests/utils.py +++ b/bids/tests/utils.py @@ -1,7 +1,28 @@ -''' Test-related utilities ''' +""" Test-related utilities """ -from os.path import join, dirname, abspath +from pathlib import Path +from ..utils import write_derivative_description +from .. import BIDSLayout +def get_test_data_path(return_type="str"): + """ + :param return_type: Specify the type of object returned. Can be 'str' + (default, for backward-compatibility) or 'Path' for pathlib.Path type. + :return: The path for testing data. + """ -def get_test_data_path(): - return join(dirname(abspath(__file__)), 'data') + path = Path(__file__).resolve().parent / 'data' + if return_type == "str": + return str(path) + elif return_type == "Path": + return path + else: + raise ValueError("return_type can be 'str' or 'Path. Got {}.".format(return_type)) + + +def test_write_derivative_description(exist_ok=True): + """Test write_derivative_description(source_dir, name, bids_version='1.1.1', **desc_kwargs). """ + + source_dir = get_test_data_path("Path") / '7t_trt' + write_derivative_description(source_dir, name="test", bids_version='1.1.1', exist_ok=exist_ok) + BIDSLayout(source_dir, derivatives=True) diff --git a/bids/utils.py b/bids/utils.py index 902f1e570..fc23ff8e8 100644 --- a/bids/utils.py +++ b/bids/utils.py @@ -2,16 +2,19 @@ import re import os +import json +from pathlib import Path +from packaging.version import Version def listify(obj): - ''' Wraps all non-list or tuple objects in a list; provides a simple way - to accept flexible arguments. ''' + """ Wraps all non-list or tuple objects in a list; provides a simple way + to accept flexible arguments. """ return obj if isinstance(obj, (list, tuple, type(None))) else [obj] def matches_entities(obj, entities, strict=False): - ''' Checks whether an object's entities match the input. ''' + """ Checks whether an object's entities match the input. """ if strict and set(obj.entities.keys()) != set(entities.keys()): return False @@ -28,9 +31,9 @@ def matches_entities(obj, entities, strict=False): def natural_sort(l, field=None): - ''' - based on snippet found at http://stackoverflow.com/a/4836734/2445984 - ''' + """ + based on snippet found at http://stackoverflow.com/a/4836734/2445984 + """ convert = lambda text: int(text) if text.isdigit() else text.lower() def alphanum_key(key): @@ -44,7 +47,8 @@ def alphanum_key(key): def convert_JSON(j): """ Recursively convert CamelCase keys to snake_case. - From: https://stackoverflow.com/questions/17156078/converting-identifier-naming-between-camelcase-and-underscores-during-json-seria + From: https://stackoverflow.com/questions/17156078/converting-identifier- + naming-between-camelcase-and-underscores-during-json-seria """ def camel_to_snake(s): @@ -78,13 +82,15 @@ def convertArray(a): def splitext(path): """splitext for paths with directories that may contain dots. - From https://stackoverflow.com/questions/5930036/separating-file-extensions-using-python-os-path-module""" + From https://stackoverflow.com/questions/5930036/separating-file + -extensions-using-python-os-path-module""" li = [] path_without_extensions = os.path.join(os.path.dirname(path), - os.path.basename(path).split(os.extsep)[0]) + os.path.basename(path).split(os.extsep)[0]) extensions = os.path.basename(path).split(os.extsep)[1:] li.append(path_without_extensions) - # li.append(extensions) if you want extensions in another list inside the list that is returned. + # li.append(extensions) if you want extensions in another + # list inside the list that is returned. li.extend(extensions) return li @@ -109,3 +115,78 @@ def make_bidsfile(filename): Cls = getattr(models, cls) return Cls(filename) + + +# As per https://bids.neuroimaging.io/bids_spec1.1.1.pdf +desc_fields = { + Version("1.1.1"): { + "required": ["Name", "BIDSVersion"], + "recommended": ["License"], + "optional": ["Authors", "Acknowledgements", "HowToAcknowledge", + "Funding", "ReferencesAndLinks", "DatasetDOI"] + } +} + + +def get_description_fields(version, type_): + 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_derivative_description(source_dir, name, bids_version='1.1.1', exist_ok=False, **desc_kwargs): + + """ + Write a dataset_description.json file for a new derivative folder. + source_dir : Directory of the BIDS dataset that has been derived. + This dataset can itself be a derivative. + name : Name of the derivative dataset. + bid_version: Version of the BIDS standard. + desc_kwargs: Dictionary of entries that should be added to the + dataset_description.json file. + exist_ok : Control the behavior of pathlib.Path.mkdir when a derivative folder + with this name already exists. + """ + if source_dir is str: + source_dir = Path(source_dir) + + deriv_dir = source_dir / "derivatives" / name + + # I found nothing about the requirement of a PipelineDescription.Name + # for derivatives in https://bids.neuroimaging.io/bids_spec1.1.1.pdf, but it + # is required by BIDSLayout(..., derivatives=True) + desc = { + 'Name': name, + 'BIDSVersion': bids_version, + 'PipelineDescription': { + "Name": name + } + } + desc.update(desc_kwargs) + + 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.") + with fname.open() as fobj: + orig_desc = json.load(fobj) + + 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] + + 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)) + + deriv_dir.mkdir(parents=True, exist_ok=exist_ok) + with (deriv_dir / 'dataset_description.json').open('w') as fobj: + json.dump(desc, fobj, indent=4) diff --git a/setup.py b/setup.py index f2290b836..90a9f5271 100755 --- a/setup.py +++ b/setup.py @@ -14,4 +14,6 @@ setup(name="pybids", version=versioneer.get_version(), cmdclass=versioneer.get_cmdclass(), - setup_requires=SETUP_REQUIRES) + setup_requires=SETUP_REQUIRES, + install_requires=['packaging'] + ) From ea9793e60e43270e476de31d72f757fa7c49b4c9 Mon Sep 17 00:00:00 2001 From: Christian O'Reilly Date: Fri, 22 Nov 2019 14:19:30 -0500 Subject: [PATCH 02/10] Adding depencies because CI complains. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 90a9f5271..e2b3aa9ed 100755 --- a/setup.py +++ b/setup.py @@ -15,5 +15,5 @@ version=versioneer.get_version(), cmdclass=versioneer.get_cmdclass(), setup_requires=SETUP_REQUIRES, - install_requires=['packaging'] + install_requires=['packaging', 'sqlalchemy'] ) From d0c7bb5b2d167a655afa329f53fcf4916b66c196 Mon Sep 17 00:00:00 2001 From: Christian O'Reilly Date: Fri, 22 Nov 2019 14:29:04 -0500 Subject: [PATCH 03/10] Fixing the requirements. --- bids/tests/utils.py | 1 + setup.cfg | 1 + setup.py | 1 - 3 files changed, 2 insertions(+), 1 deletion(-) diff --git a/bids/tests/utils.py b/bids/tests/utils.py index 5516d7e94..a525650a1 100644 --- a/bids/tests/utils.py +++ b/bids/tests/utils.py @@ -4,6 +4,7 @@ from ..utils import write_derivative_description from .. import BIDSLayout + def get_test_data_path(return_type="str"): """ :param return_type: Specify the type of object returned. Can be 'str' diff --git a/setup.cfg b/setup.cfg index 676ee0dc9..ee16e3ae9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -30,6 +30,7 @@ install_requires = sqlalchemy bids-validator num2words + packaging tests_require = pytest >=3.3 mock diff --git a/setup.py b/setup.py index e2b3aa9ed..3b4ccb8a2 100755 --- a/setup.py +++ b/setup.py @@ -15,5 +15,4 @@ version=versioneer.get_version(), cmdclass=versioneer.get_cmdclass(), setup_requires=SETUP_REQUIRES, - install_requires=['packaging', 'sqlalchemy'] ) From 40e11713e36437a046beb434f3224647ef753ebb Mon Sep 17 00:00:00 2001 From: Christian O'Reilly Date: Fri, 22 Nov 2019 14:50:49 -0500 Subject: [PATCH 04/10] Moving write_derivative_description to bids/layout/utils.py --- bids/layout/tests/test_utils.py | 13 ++++++ bids/layout/utils.py | 78 +++++++++++++++++++++++++++++++++ bids/tests/utils.py | 10 ----- bids/utils.py | 78 --------------------------------- 4 files changed, 91 insertions(+), 88 deletions(-) create mode 100644 bids/layout/tests/test_utils.py create mode 100644 bids/layout/utils.py diff --git a/bids/layout/tests/test_utils.py b/bids/layout/tests/test_utils.py new file mode 100644 index 000000000..1e8cf670d --- /dev/null +++ b/bids/layout/tests/test_utils.py @@ -0,0 +1,13 @@ +""" Test-related utilities """ + +from ..utils import write_derivative_description +from ...tests import get_test_data_path +from .. import BIDSLayout + + +def test_write_derivative_description(exist_ok=True): + """Test write_derivative_description(source_dir, name, bids_version='1.1.1', **desc_kwargs). """ + + source_dir = get_test_data_path("Path") / '7t_trt' + write_derivative_description(source_dir, name="test", bids_version='1.1.1', exist_ok=exist_ok) + BIDSLayout(source_dir, derivatives=True) diff --git a/bids/layout/utils.py b/bids/layout/utils.py new file mode 100644 index 000000000..48215ec2e --- /dev/null +++ b/bids/layout/utils.py @@ -0,0 +1,78 @@ +import json +from pathlib import Path +from packaging.version import Version + + +# As per https://bids.neuroimaging.io/bids_spec1.1.1.pdf +desc_fields = { + Version("1.1.1"): { + "required": ["Name", "BIDSVersion"], + "recommended": ["License"], + "optional": ["Authors", "Acknowledgements", "HowToAcknowledge", + "Funding", "ReferencesAndLinks", "DatasetDOI"] + } +} + + +def get_description_fields(version, type_): + 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_derivative_description(source_dir, name, bids_version='1.1.1', exist_ok=False, **desc_kwargs): + + """ + Write a dataset_description.json file for a new derivative folder. + source_dir : Directory of the BIDS dataset that has been derived. + This dataset can itself be a derivative. + name : Name of the derivative dataset. + bid_version: Version of the BIDS standard. + desc_kwargs: Dictionary of entries that should be added to the + dataset_description.json file. + exist_ok : Control the behavior of pathlib.Path.mkdir when a derivative folder + with this name already exists. + """ + if source_dir is str: + source_dir = Path(source_dir) + + deriv_dir = source_dir / "derivatives" / name + + # I found nothing about the requirement of a PipelineDescription.Name + # for derivatives in https://bids.neuroimaging.io/bids_spec1.1.1.pdf, but it + # is required by BIDSLayout(..., derivatives=True) + desc = { + 'Name': name, + 'BIDSVersion': bids_version, + 'PipelineDescription': { + "Name": name + } + } + desc.update(desc_kwargs) + + 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.") + with fname.open() as fobj: + orig_desc = json.load(fobj) + + 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] + + 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)) + + deriv_dir.mkdir(parents=True, exist_ok=exist_ok) + with (deriv_dir / 'dataset_description.json').open('w') as fobj: + json.dump(desc, fobj, indent=4) diff --git a/bids/tests/utils.py b/bids/tests/utils.py index a525650a1..aba50b23c 100644 --- a/bids/tests/utils.py +++ b/bids/tests/utils.py @@ -1,8 +1,6 @@ """ Test-related utilities """ from pathlib import Path -from ..utils import write_derivative_description -from .. import BIDSLayout def get_test_data_path(return_type="str"): @@ -19,11 +17,3 @@ def get_test_data_path(return_type="str"): return path else: raise ValueError("return_type can be 'str' or 'Path. Got {}.".format(return_type)) - - -def test_write_derivative_description(exist_ok=True): - """Test write_derivative_description(source_dir, name, bids_version='1.1.1', **desc_kwargs). """ - - source_dir = get_test_data_path("Path") / '7t_trt' - write_derivative_description(source_dir, name="test", bids_version='1.1.1', exist_ok=exist_ok) - BIDSLayout(source_dir, derivatives=True) diff --git a/bids/utils.py b/bids/utils.py index fc23ff8e8..07f41b4be 100644 --- a/bids/utils.py +++ b/bids/utils.py @@ -2,9 +2,6 @@ import re import os -import json -from pathlib import Path -from packaging.version import Version def listify(obj): @@ -115,78 +112,3 @@ def make_bidsfile(filename): Cls = getattr(models, cls) return Cls(filename) - - -# As per https://bids.neuroimaging.io/bids_spec1.1.1.pdf -desc_fields = { - Version("1.1.1"): { - "required": ["Name", "BIDSVersion"], - "recommended": ["License"], - "optional": ["Authors", "Acknowledgements", "HowToAcknowledge", - "Funding", "ReferencesAndLinks", "DatasetDOI"] - } -} - - -def get_description_fields(version, type_): - 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_derivative_description(source_dir, name, bids_version='1.1.1', exist_ok=False, **desc_kwargs): - - """ - Write a dataset_description.json file for a new derivative folder. - source_dir : Directory of the BIDS dataset that has been derived. - This dataset can itself be a derivative. - name : Name of the derivative dataset. - bid_version: Version of the BIDS standard. - desc_kwargs: Dictionary of entries that should be added to the - dataset_description.json file. - exist_ok : Control the behavior of pathlib.Path.mkdir when a derivative folder - with this name already exists. - """ - if source_dir is str: - source_dir = Path(source_dir) - - deriv_dir = source_dir / "derivatives" / name - - # I found nothing about the requirement of a PipelineDescription.Name - # for derivatives in https://bids.neuroimaging.io/bids_spec1.1.1.pdf, but it - # is required by BIDSLayout(..., derivatives=True) - desc = { - 'Name': name, - 'BIDSVersion': bids_version, - 'PipelineDescription': { - "Name": name - } - } - desc.update(desc_kwargs) - - 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.") - with fname.open() as fobj: - orig_desc = json.load(fobj) - - 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] - - 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)) - - deriv_dir.mkdir(parents=True, exist_ok=exist_ok) - with (deriv_dir / 'dataset_description.json').open('w') as fobj: - json.dump(desc, fobj, indent=4) From ea5ed737d32874743cf97cb2928b7b84c4464573 Mon Sep 17 00:00:00 2001 From: Christian O'Reilly Date: Thu, 5 Dec 2019 14:07:26 -0500 Subject: [PATCH 05/10] Edits for the review of the PR adding write_derivative_description --- bids/layout/utils.py | 62 ++++++++++++++++++++++++------------------ bids/layout/writing.py | 2 +- bids/tests/utils.py | 20 +++++++++----- setup.py | 3 +- 4 files changed, 50 insertions(+), 37 deletions(-) diff --git a/bids/layout/utils.py b/bids/layout/utils.py index 48215ec2e..9a7b49608 100644 --- a/bids/layout/utils.py +++ b/bids/layout/utils.py @@ -25,27 +25,34 @@ def get_description_fields(version, type_): return desc_fields[max(desc_fields.keys())][type_] -def write_derivative_description(source_dir, name, bids_version='1.1.1', exist_ok=False, **desc_kwargs): +def write_derivative_description(source_dir, name, bids_version='1.1.1', 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. """ - Write a dataset_description.json file for a new derivative folder. - source_dir : Directory of the BIDS dataset that has been derived. - This dataset can itself be a derivative. - name : Name of the derivative dataset. - bid_version: Version of the BIDS standard. - desc_kwargs: Dictionary of entries that should be added to the - dataset_description.json file. - exist_ok : Control the behavior of pathlib.Path.mkdir when a derivative folder - with this name already exists. - """ - if source_dir is str: - source_dir = Path(source_dir) + source_dir = Path(source_dir) deriv_dir = source_dir / "derivatives" / name - # I found nothing about the requirement of a PipelineDescription.Name - # for derivatives in https://bids.neuroimaging.io/bids_spec1.1.1.pdf, but it - # is required by BIDSLayout(..., derivatives=True) desc = { 'Name': name, 'BIDSVersion': bids_version, @@ -53,26 +60,27 @@ def write_derivative_description(source_dir, name, bids_version='1.1.1', exist_o "Name": name } } - desc.update(desc_kwargs) 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.") - with fname.open() as fobj: - orig_desc = json.load(fobj) + 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_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] 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)) deriv_dir.mkdir(parents=True, exist_ok=exist_ok) - with (deriv_dir / 'dataset_description.json').open('w') as fobj: - json.dump(desc, fobj, indent=4) + Path.write_text(deriv_dir / 'dataset_description.json', json.dumps(desc, indent=4)) diff --git a/bids/layout/writing.py b/bids/layout/writing.py index 7e5de741a..2b2fec6d1 100644 --- a/bids/layout/writing.py +++ b/bids/layout/writing.py @@ -91,7 +91,7 @@ def build_path(entities, path_patterns, strict=False): """ path_patterns = listify(path_patterns) - # Loop over available patherns, return first one that matches all + # Loop over available patterns, return first one that matches all for pattern in path_patterns: # If strict, all entities must be contained in the pattern if strict: diff --git a/bids/tests/utils.py b/bids/tests/utils.py index aba50b23c..c8d5ac6f6 100644 --- a/bids/tests/utils.py +++ b/bids/tests/utils.py @@ -1,19 +1,25 @@ """ Test-related utilities """ - from pathlib import Path +import pkg_resources def get_test_data_path(return_type="str"): """ - :param return_type: Specify the type of object returned. Can be 'str' - (default, for backward-compatibility) or 'Path' for pathlib.Path type. - :return: The path for testing data. + 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. + + Returns + ------- + The path for testing data. """ - path = Path(__file__).resolve().parent / 'data' + path = pkg_resources.resource_filename('bids', 'tests/data') if return_type == "str": - return str(path) - elif return_type == "Path": 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.py b/setup.py index 3b4ccb8a2..f2290b836 100755 --- a/setup.py +++ b/setup.py @@ -14,5 +14,4 @@ setup(name="pybids", version=versioneer.get_version(), cmdclass=versioneer.get_cmdclass(), - setup_requires=SETUP_REQUIRES, - ) + setup_requires=SETUP_REQUIRES) From 3d42e0df3e1c8cc64178c5d00d050adc8931030e Mon Sep 17 00:00:00 2001 From: Christian O'Reilly Date: Thu, 5 Dec 2019 14:26:02 -0500 Subject: [PATCH 06/10] Revert pep8-compliance changes to bids/tests/utils.py. --- bids/tests/utils.py | 2 +- bids/utils.py | 23 ++++++++++------------- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/bids/tests/utils.py b/bids/tests/utils.py index c8d5ac6f6..10f84b079 100644 --- a/bids/tests/utils.py +++ b/bids/tests/utils.py @@ -1,4 +1,4 @@ -""" Test-related utilities """ +''' Test-related utilities ''' from pathlib import Path import pkg_resources diff --git a/bids/utils.py b/bids/utils.py index 07f41b4be..902f1e570 100644 --- a/bids/utils.py +++ b/bids/utils.py @@ -5,13 +5,13 @@ def listify(obj): - """ Wraps all non-list or tuple objects in a list; provides a simple way - to accept flexible arguments. """ + ''' Wraps all non-list or tuple objects in a list; provides a simple way + to accept flexible arguments. ''' return obj if isinstance(obj, (list, tuple, type(None))) else [obj] def matches_entities(obj, entities, strict=False): - """ Checks whether an object's entities match the input. """ + ''' Checks whether an object's entities match the input. ''' if strict and set(obj.entities.keys()) != set(entities.keys()): return False @@ -28,9 +28,9 @@ def matches_entities(obj, entities, strict=False): def natural_sort(l, field=None): - """ - based on snippet found at http://stackoverflow.com/a/4836734/2445984 - """ + ''' + based on snippet found at http://stackoverflow.com/a/4836734/2445984 + ''' convert = lambda text: int(text) if text.isdigit() else text.lower() def alphanum_key(key): @@ -44,8 +44,7 @@ def alphanum_key(key): def convert_JSON(j): """ Recursively convert CamelCase keys to snake_case. - From: https://stackoverflow.com/questions/17156078/converting-identifier- - naming-between-camelcase-and-underscores-during-json-seria + From: https://stackoverflow.com/questions/17156078/converting-identifier-naming-between-camelcase-and-underscores-during-json-seria """ def camel_to_snake(s): @@ -79,15 +78,13 @@ def convertArray(a): def splitext(path): """splitext for paths with directories that may contain dots. - From https://stackoverflow.com/questions/5930036/separating-file - -extensions-using-python-os-path-module""" + From https://stackoverflow.com/questions/5930036/separating-file-extensions-using-python-os-path-module""" li = [] path_without_extensions = os.path.join(os.path.dirname(path), - os.path.basename(path).split(os.extsep)[0]) + os.path.basename(path).split(os.extsep)[0]) extensions = os.path.basename(path).split(os.extsep)[1:] li.append(path_without_extensions) - # li.append(extensions) if you want extensions in another - # list inside the list that is returned. + # li.append(extensions) if you want extensions in another list inside the list that is returned. li.extend(extensions) return li From 4b32359a96ce0a49066f31cae8d9ccea2c656a76 Mon Sep 17 00:00:00 2001 From: Christian O'Reilly Date: Thu, 5 Dec 2019 17:40:25 -0500 Subject: [PATCH 07/10] Python 3.5 compatibility for use of Path objects. --- bids/layout/layout.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bids/layout/layout.py b/bids/layout/layout.py index 266abd4e7..b80b78e54 100644 --- a/bids/layout/layout.py +++ b/bids/layout/layout.py @@ -199,7 +199,7 @@ def __init__(self, root, validate=True, absolute_paths=True, regex_search=False, database_path=None, database_file=None, reset_database=False, index_metadata=True): """Initialize BIDSLayout.""" - self.root = root + self.root = str(root) self.validate = validate self.absolute_paths = absolute_paths self.derivatives = {} From b5ca110df8707053dfad0046a40d2f73c9428b17 Mon Sep 17 00:00:00 2001 From: Christian O'Reilly Date: Thu, 5 Dec 2019 18:42:30 -0500 Subject: [PATCH 08/10] Update layout.py --- bids/layout/layout.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bids/layout/layout.py b/bids/layout/layout.py index b80b78e54..680ecc4e7 100644 --- a/bids/layout/layout.py +++ b/bids/layout/layout.py @@ -208,7 +208,7 @@ def __init__(self, root, validate=True, absolute_paths=True, self.config_filename = config_filename # Store original init arguments as dictionary self._init_args = self._sanitize_init_args( - root=root, validate=validate, absolute_paths=absolute_paths, + root=self.root, validate=validate, absolute_paths=absolute_paths, derivatives=derivatives, ignore=ignore, force_index=force_index, index_metadata=index_metadata, config=config) @@ -267,7 +267,7 @@ def __init__(self, root, validate=True, absolute_paths=True, # Add derivatives if any are found if derivatives: if derivatives is True: - derivatives = os.path.join(root, 'derivatives') + derivatives = os.path.join(self.root, 'derivatives') self.add_derivatives( derivatives, parent_database_path=database_path, validate=validate, absolute_paths=absolute_paths, From 00229f6c984f1b8a6b08a609f53103495c8d956e Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sat, 29 Jan 2022 10:26:24 +0100 Subject: [PATCH 09/10] fix to make tests pass --- bids/layout/tests/test_utils.py | 34 ++++++++++++++++++++++++--------- bids/layout/utils.py | 30 ++++++++++++++++++++--------- 2 files changed, 46 insertions(+), 18 deletions(-) diff --git a/bids/layout/tests/test_utils.py b/bids/layout/tests/test_utils.py index e7a4e3500..6156bf0ae 100644 --- a/bids/layout/tests/test_utils.py +++ b/bids/layout/tests/test_utils.py @@ -7,7 +7,14 @@ from bids.exceptions import ConfigError from ..models import Entity, Config + from ..utils import BIDSMetadata, parse_file_entities, add_config_paths +from ..utils import write_derivative_description +from ..utils import get_description_fields + +from ...tests import get_test_data_path + + def test_bidsmetadata_class(): @@ -61,16 +68,25 @@ def test_add_config_paths(): config = Config.load('dummy') assert 'subject' in config.entities -""" Test-related utilities """ - -from ..utils import write_derivative_description -from ...tests import get_test_data_path -from .. import BIDSLayout - def test_write_derivative_description(exist_ok=True): - """Test write_derivative_description(source_dir, name, bids_version='1.1.1', **desc_kwargs). """ source_dir = get_test_data_path("Path") / '7t_trt' - write_derivative_description(source_dir, name="test", bids_version='1.1.1', exist_ok=exist_ok) - BIDSLayout(source_dir, derivatives=True) + write_derivative_description(source_dir, pipeline_name="test", + pipeline_version="0.1.0", + bids_version='1.6.1', + exist_ok=exist_ok) + bids.BIDSLayout(source_dir, derivatives=True) + + +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 7a432a488..a3b3f5783 100644 --- a/bids/layout/utils.py +++ b/bids/layout/utils.py @@ -103,18 +103,27 @@ def add_config_paths(**kwargs): from packaging.version import Version -# As per https://bids.neuroimaging.io/bids_spec1.1.1.pdf 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, type_): +def get_description_fields(version:str, type_:str): if isinstance(version, str): version = Version(version) if not isinstance(version, Version): @@ -125,7 +134,8 @@ def get_description_fields(version, type_): return desc_fields[max(desc_fields.keys())][type_] -def write_derivative_description(source_dir, name, bids_version='1.1.1', exist_ok=False, +def write_derivative_description(source_dir, pipeline_name, pipeline_version="0.1.0", + bids_version='1.1.1', exist_ok=False, propagate=False, **desc_kwargs): """Write a dataset_description.json file for a new derivative folder. @@ -151,14 +161,16 @@ def write_derivative_description(source_dir, name, bids_version='1.1.1', exist_o """ source_dir = Path(source_dir) - deriv_dir = source_dir / "derivatives" / name + deriv_dir = source_dir / "derivatives" / pipeline_name desc = { - 'Name': name, + 'Name': pipeline_name, 'BIDSVersion': bids_version, - 'PipelineDescription': { - "Name": name - } + 'GeneratedBy': [{ + "Name": pipeline_name, + "Version": pipeline_version + }], + "SourceDatasets": "" } fname = source_dir / 'dataset_description.json' From 1af6978678b7bb5dcb4032dc28704aa515ac4894 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sat, 29 Jan 2022 11:31:00 +0100 Subject: [PATCH 10/10] handle simple raw dataset_description and let user decide where to ouput derivatives --- bids/layout/tests/test_utils.py | 46 ++++++++++++-- bids/layout/utils.py | 109 ++++++++++++++++++-------------- 2 files changed, 104 insertions(+), 51 deletions(-) diff --git a/bids/layout/tests/test_utils.py b/bids/layout/tests/test_utils.py index 6156bf0ae..dda88adc9 100644 --- a/bids/layout/tests/test_utils.py +++ b/bids/layout/tests/test_utils.py @@ -3,13 +3,17 @@ 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_derivative_description +from ..utils import write_description from ..utils import get_description_fields from ...tests import get_test_data_path @@ -68,16 +72,48 @@ def test_add_config_paths(): 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): -def test_write_derivative_description(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_derivative_description(source_dir, pipeline_name="test", - pipeline_version="0.1.0", - bids_version='1.6.1', + + 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(): diff --git a/bids/layout/utils.py b/bids/layout/utils.py index a3b3f5783..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. """ @@ -98,10 +101,6 @@ def add_config_paths(**kwargs): kwargs.update(**cf.get_option('config_paths')) cf.set_option('config_paths', kwargs) -import json -from pathlib import Path -from packaging.version import Version - desc_fields = { Version("1.1.1"): { @@ -122,7 +121,6 @@ def add_config_paths(**kwargs): } } - def get_description_fields(version:str, type_:str): if isinstance(version, str): version = Version(version) @@ -134,9 +132,10 @@ def get_description_fields(version:str, type_:str): return desc_fields[max(desc_fields.keys())][type_] -def write_derivative_description(source_dir, pipeline_name, pipeline_version="0.1.0", - bids_version='1.1.1', exist_ok=False, - propagate=False, **desc_kwargs): +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 @@ -159,40 +158,58 @@ def write_derivative_description(source_dir, pipeline_name, pipeline_version="0. Dictionary of entries that should be added to the dataset_description.json file. """ - source_dir = Path(source_dir) - - deriv_dir = source_dir / "derivatives" / pipeline_name - - desc = { - 'Name': pipeline_name, - 'BIDSVersion': bids_version, - 'GeneratedBy': [{ - "Name": pipeline_name, - "Version": pipeline_version - }], - "SourceDatasets": "" - } - - 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)) - - deriv_dir.mkdir(parents=True, exist_ok=exist_ok) - Path.write_text(deriv_dir / 'dataset_description.json', json.dumps(desc, indent=4)) + + 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))