Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Adding write_derivative_description and the related test #811

Draft
wants to merge 11 commits into
base: master
Choose a base branch
from
Draft
66 changes: 66 additions & 0 deletions bids/layout/tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down Expand Up @@ -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")
118 changes: 117 additions & 1 deletion bids/layout/utils.py
Original file line number Diff line number Diff line change
@@ -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. """
Expand Down Expand Up @@ -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))
24 changes: 21 additions & 3 deletions bids/tests/utils.py
Original file line number Diff line number Diff line change
@@ -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))
1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ install_requires =
sqlalchemy <1.4.0.dev0
bids-validator
num2words
packaging
click
tests_require =
pytest >=3.3
Expand Down