diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 343c40bf..e3abdfec 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -8,10 +8,10 @@ jobs: steps: - uses: actions/checkout@v1 - - name: Set up Python 3.7 + - name: Set up Python 3.10.11 uses: actions/setup-python@v1 with: - python-version: 3.7 + python-version: 3.10.11 - name: Install dependencies run: | python -m pip install --upgrade pip @@ -19,8 +19,8 @@ jobs: pip install . - name: Run coverage run: pytest --cov --cov-report=xml - - name: Upload coverage - uses: codecov/codecov-action@v1 + - name: Upload coverage + uses: codecov/codecov-action@v3 if: always() with: token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/publish_doc.yaml b/.github/workflows/publish_doc.yaml index e5dedbf0..b8dfa94e 100644 --- a/.github/workflows/publish_doc.yaml +++ b/.github/workflows/publish_doc.yaml @@ -14,7 +14,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v3 with: - python-version: "3.10.4" + python-version: "3.10.11" - name: Upgrade pip run: | # install pip=>20.1 to use "pip cache dir" diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6aa28bbe..8b49cd5c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -13,7 +13,7 @@ jobs: - ubuntu-latest # ubuntu-18.04 - macos-latest # macOS-10.14 - windows-latest # windows-2019 - python-version: [3.7, 3.8, 3.9] + python-version: [3.8, 3.9, 3.10.11] steps: - uses: actions/checkout@v1 diff --git a/README.md b/README.md index 9cf68557..a73744a8 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,8 @@ Your friendly DICOM converter. [![PyPI version badge](https://img.shields.io/pypi/v/dcm2bids?logo=pypi&logoColor=white)](https://pypi.org/project/dcm2bids) +[![PyPI - Downloads](https://static.pepy.tech/badge/dcm2bids)](https://pypi.org/project/dcm2bids) + [![Anaconda-Server Badge](https://img.shields.io/conda/vn/conda-forge/dcm2bids?logo=anaconda&logoColor=white)](https://anaconda.org/conda-forge/dcm2bids) [![Docker container badge](https://img.shields.io/docker/v/unfmontreal/dcm2bids?label=docker&logo=docker&logoColor=white)](https://hub.docker.com/r/unfmontreal/dcm2bids) diff --git a/dcm2bids/__init__.py b/dcm2bids/__init__.py index ced1b27e..e69de29b 100644 --- a/dcm2bids/__init__.py +++ b/dcm2bids/__init__.py @@ -1,15 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -dcm2bids --------- - -Reorganising NIfTI files from dcm2niix into the Brain Imaging Data Structure -""" - - -from .dcm2bids import Dcm2bids -from .scaffold import scaffold -from .version import __version__ - -__all__ = ["__version__", "Dcm2bids", "scaffold"] diff --git a/dcm2bids/structure.py b/dcm2bids/acquisition.py similarity index 76% rename from dcm2bids/structure.py rename to dcm2bids/acquisition.py index 97b08aa4..cda538cc 100644 --- a/dcm2bids/structure.py +++ b/dcm2bids/acquisition.py @@ -4,96 +4,9 @@ import logging from os.path import join as opj -from future.utils import iteritems -from .utils import DEFAULT -from .version import __version__ - -class Participant(object): - """ Class representing a participant - - Args: - name (str): Label of your participant - session (str): Optional label of a session - """ - - def __init__(self, name, session=DEFAULT.session): - self._name = "" - self._session = "" - - self.name = name - self.session = session - - @property - def name(self): - """ - Returns: - A string 'sub-' - """ - return self._name - - @name.setter - def name(self, name): - """ Prepend 'sub-' if necessary""" - if name.startswith("sub-"): - self._name = name - - else: - self._name = "sub-" + name - - @property - def session(self): - """ - Returns: - A string 'ses-' - """ - return self._session - - @session.setter - def session(self, session): - """ Prepend 'ses-' if necessary""" - if session.strip() == "": - self._session = "" - - elif session.startswith("ses-"): - self._session = session - - else: - self._session = "ses-" + session - - @property - def directory(self): - """ The directory of the participant - - Returns: - A path 'sub-' or - 'sub-/ses-' - """ - if self.hasSession(): - return opj(self.name, self.session) - else: - return self.name - - @property - def prefix(self): - """ The prefix to build filenames - - Returns: - A string 'sub-' or - 'sub-_ses-' - """ - if self.hasSession(): - return self.name + "_" + self.session - else: - return self.name - - def hasSession(self): - """ Check if a session is set - - Returns: - Boolean - """ - return self.session.strip() != DEFAULT.session +from dcm2bids.utils.utils import DEFAULT +from dcm2bids.version import __version__ class Acquisition(object): @@ -313,7 +226,7 @@ def dstSidecarData(self, descriptions, intendedForList): data["IntendedFor"] = intendedValue # sidecarChanges - for key, value in iteritems(self.sidecarChanges): + for key, value in self.sidecarChanges.items(): data[key] = value return data diff --git a/dcm2bids/cli/__init__.py b/dcm2bids/cli/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/dcm2bids/cli/dcm2bids.py b/dcm2bids/cli/dcm2bids.py new file mode 100644 index 00000000..3b3a007f --- /dev/null +++ b/dcm2bids/cli/dcm2bids.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Reorganising NIfTI files from dcm2niix into the Brain Imaging Data Structure +""" + +import argparse + +from dcm2bids.dcm2bids_gen import Dcm2BidsGen +from dcm2bids.utils.tools import check_latest +from dcm2bids.utils.utils import DEFAULT +from dcm2bids.version import __version__ + +def _build_arg_parser(): + p = argparse.ArgumentParser(description=__doc__, epilog=DEFAULT.doc, + formatter_class=argparse.RawTextHelpFormatter) + + p.add_argument("-d", "--dicom_dir", + required=True, nargs="+", + help="DICOM directory(ies).") + + p.add_argument("-p", "--participant", + required=True, + help="Participant ID.") + + p.add_argument("-s", "--session", + required=False, + default=DEFAULT.cliSession, + help="Session ID. [%(default)s]") + + p.add_argument("-c", "--config", + required=True, + help="JSON configuration file (see example/config.json).") + + p.add_argument("-o", "--output_dir", + required=False, + default=DEFAULT.cliOutputDir, + help="Output BIDS directory. [%(default)s]") + + p.add_argument("--forceDcm2niix", + action="store_true", + help="Overwrite previous temporary dcm2niix " + "output if it exists.") + + p.add_argument("--clobber", + action="store_true", + help="Overwrite output if it exists.") + + p.add_argument("-l", "--log_level", + required=False, + default=DEFAULT.cliLogLevel, + choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], + help="Set logging level. [%(default)s]") + + p.add_argument("-v", "--version", + action="version", + version=f"dcm2bids version:\t{__version__}\nBased on BIDS version:\t{DEFAULT.bids_version}", + help="Report dcm2bids version and the BIDS version.") + + return p + + +def main(): + parser = _build_arg_parser() + args = parser.parse_args() + + check_latest() + check_latest("dcm2niix") + + app = Dcm2BidsGen(**vars(args)) + return app.run() + + +if __name__ == "__main__": + main() diff --git a/dcm2bids/helper.py b/dcm2bids/cli/dcm2bids_helper.py similarity index 57% rename from dcm2bids/helper.py rename to dcm2bids/cli/dcm2bids_helper.py index bcb77ffd..be5d35b4 100644 --- a/dcm2bids/helper.py +++ b/dcm2bids/cli/dcm2bids_helper.py @@ -3,28 +3,25 @@ """helper module""" import argparse -import os -from pathlib import Path -import sys +from os.path import join as opj -from dcm2bids.dcm2niix import Dcm2niix -from dcm2bids.utils import DEFAULT, assert_dirs_empty +from dcm2bids.dcm2niix_gen import Dcm2niixGen +from dcm2bids.utils.args import assert_dirs_empty +from dcm2bids.utils.utils import DEFAULT def _build_arg_parser(): - p = argparse.ArgumentParser(description=__doc__, epilog=DEFAULT.EPILOG, + p = argparse.ArgumentParser(description=__doc__, epilog=DEFAULT.doc, formatter_class=argparse.RawTextHelpFormatter) p.add_argument("-d", "--dicom_dir", - type=Path, required=True, nargs="+", help="DICOM files directory.") p.add_argument("-o", "--output_dir", - required=False, default=Path.cwd(), - type=Path, - help="Output BIDS directory. " - "(Default: %(default)s)") + required=False, default=DEFAULT.cliOutputDir, + help="Output BIDS directory." + " (Default: %(default)s)") p.add_argument('--force', dest='overwrite', action='store_true', @@ -37,13 +34,17 @@ def main(): """Let's go""" parser = _build_arg_parser() args = parser.parse_args() - out_folder = args.output_dir / DEFAULT.tmpDirName / DEFAULT.helperDir + + out_folder = opj(args.output_dir, 'tmp_dcm2bids', 'helper') assert_dirs_empty(parser, args, out_folder) - app = Dcm2niix(dicomDirs=args.dicom_dir, bidsDir=args.output_dir) + + app = Dcm2niixGen(dicomDirs=args.dicom_dir, bidsDir=args.output_dir) rsl = app.run() - print(f"Example in: {out_folder}") + print("Example in:") + print(opj(args.output_dir, DEFAULT.tmpDirName, DEFAULT.helperDir)) + return rsl if __name__ == "__main__": - sys.exit(main()) + main() diff --git a/dcm2bids/cli/dcm2bids_scaffold.py b/dcm2bids/cli/dcm2bids_scaffold.py new file mode 100644 index 00000000..94329fee --- /dev/null +++ b/dcm2bids/cli/dcm2bids_scaffold.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" + Create basic BIDS files and directories. + + Based on the material provided by + https://github.com/bids-standard/bids-starter-kit +""" + + +import argparse +import datetime +import logging +import os +from os.path import join as opj + +from dcm2bids.utils.io import write_txt +from dcm2bids.utils.args import add_overwrite_arg, assert_dirs_empty +from dcm2bids.utils.utils import DEFAULT +from dcm2bids.utils.scaffold import bids_starter_kit + + +def _build_arg_parser(): + p = argparse.ArgumentParser(description=__doc__, epilog=DEFAULT.doc, + formatter_class=argparse.RawTextHelpFormatter) + + p.add_argument("-o", "--output_dir", + required=False, + default=DEFAULT.cliOutputDir, + help="Output BIDS directory. Default: [%(default)s]") + + add_overwrite_arg(p) + return p + + +def main(): + parser = _build_arg_parser() + args = parser.parse_args() + + assert_dirs_empty(parser, args, args.output_dir) + + for _ in ["code", "derivatives", "sourcedata"]: + os.makedirs(opj(args.output_dir, _), exist_ok=True) + + logging.info("The files used to create your BIDS directory comes from" + "https://github.com/bids-standard/bids-starter-kit") + # CHANGES + write_txt(opj(args.output_dir, "CHANGES"), + bids_starter_kit.CHANGES.replace('DATE', + datetime.date.today().strftime("%Y-%m-%d"))) + + # dataset_description + write_txt(opj(args.output_dir, "dataset_description"), + bids_starter_kit.dataset_description.replace("BIDS_VERSION", + DEFAULT.bids_version)) + + # participants.json + write_txt(opj(args.output_dir, "participants.json"), + bids_starter_kit.participants_json) + + # participants.tsv + write_txt(opj(args.output_dir, "participants.tsv"), + bids_starter_kit.participants_tsv) + + # README + write_txt(opj(args.output_dir, "README"), + bids_starter_kit.README) + + +if __name__ == "__main__": + main() diff --git a/dcm2bids/dcm2bids.py b/dcm2bids/dcm2bids_gen.py similarity index 92% rename from dcm2bids/dcm2bids.py rename to dcm2bids/dcm2bids_gen.py index e6e9fbf2..d49db765 100644 --- a/dcm2bids/dcm2bids.py +++ b/dcm2bids/dcm2bids_gen.py @@ -13,16 +13,16 @@ from datetime import datetime from glob import glob -from dcm2bids.dcm2niix import Dcm2niix -from dcm2bids.logger import setup_logging +from dcm2bids.dcm2niix_gen import Dcm2niixGen +from dcm2bids.utils.logger import setup_logging from dcm2bids.sidecar import Sidecar, SidecarPairing -from dcm2bids.structure import Participant -from dcm2bids.utils import (DEFAULT, load_json, save_json, - splitext_, run_shell_command, valid_path) -from dcm2bids.version import __version__, check_latest, dcm2niix_version +from dcm2bids.participant import Participant +from dcm2bids.utils.utils import DEFAULT, run_shell_command +from dcm2bids.utils.io import load_json, save_json, valid_path +from dcm2bids.utils.tools import check_latest, dcm2niix_version +from dcm2bids.version import __version__ - -class Dcm2bids(object): +class Dcm2BidsGen(object): """ Object to handle dcm2bids execution steps Args: @@ -97,7 +97,7 @@ def set_logger(self): def run(self): """Run dcm2bids""" - dcm2niix = Dcm2niix( + dcm2niix = Dcm2niixGen( self.dicomDirs, self.bidsDir, self.participant, @@ -139,13 +139,13 @@ def move(self, acquisition, intendedForList): for srcFile in glob(acquisition.srcRoot + ".*"): ext = Path(srcFile).suffixes - ext = [curr_ext for curr_ext in ext if curr_ext in ['.nii','.gz', + ext = [curr_ext for curr_ext in ext if curr_ext in ['.nii', '.gz', '.json', - '.bval','.bvec']] + '.bval', '.bvec']] dstFile = (self.bidsDir / acquisition.dstRoot).with_suffix("".join(ext)) - dstFile.parent.mkdir(parents = True, exist_ok = True) + dstFile.parent.mkdir(parents=True, exist_ok=True) # checking if destination file exists if dstFile.exists(): @@ -159,11 +159,7 @@ def move(self, acquisition, intendedForList): continue # it's an anat nifti file and the user using a deface script - if ( - self.config.get("defaceTpl") - and acquisition.dataType == "func" - and ".nii" in ext - ): + if (self.config.get("defaceTpl") and acquisition.dataType == "func" and ".nii" in ext): try: os.remove(dstFile) except FileNotFoundError: @@ -244,7 +240,7 @@ def main(): parser = _build_arg_parser() args = parser.parse_args() - app = Dcm2bids(**vars(args)) + app = Dcm2BidsGen(**vars(args)) return app.run() diff --git a/dcm2bids/dcm2niix.py b/dcm2bids/dcm2niix_gen.py similarity index 97% rename from dcm2bids/dcm2niix.py rename to dcm2bids/dcm2niix_gen.py index 60ac51ae..0707bc60 100644 --- a/dcm2bids/dcm2niix.py +++ b/dcm2bids/dcm2niix_gen.py @@ -4,14 +4,14 @@ import logging import os -from pathlib import Path import shlex import shutil from glob import glob -from .utils import DEFAULT, run_shell_command +from dcm2bids.utils.utils import DEFAULT, run_shell_command -class Dcm2niix(object): + +class Dcm2niixGen(object): """ Object to handle dcm2niix execution Args: diff --git a/dcm2bids/participant.py b/dcm2bids/participant.py new file mode 100644 index 00000000..bf6a5827 --- /dev/null +++ b/dcm2bids/participant.py @@ -0,0 +1,95 @@ +# -*- coding: utf-8 -*- + +"""Participant class""" + +from os.path import join as opj + +from dcm2bids.utils.utils import DEFAULT +from dcm2bids.version import __version__ + + +class Participant(object): + """ Class representing a participant + + Args: + name (str): Label of your participant + session (str): Optional label of a session + """ + + def __init__(self, name, session=DEFAULT.session): + self._name = "" + self._session = "" + + self.name = name + self.session = session + + @property + def name(self): + """ + Returns: + A string 'sub-' + """ + return self._name + + @name.setter + def name(self, name): + """ Prepend 'sub-' if necessary""" + if name.startswith("sub-"): + self._name = name + + else: + self._name = "sub-" + name + + @property + def session(self): + """ + Returns: + A string 'ses-' + """ + return self._session + + @session.setter + def session(self, session): + """ Prepend 'ses-' if necessary""" + if session.strip() == "": + self._session = "" + + elif session.startswith("ses-"): + self._session = session + + else: + self._session = "ses-" + session + + @property + def directory(self): + """ The directory of the participant + + Returns: + A path 'sub-' or + 'sub-/ses-' + """ + if self.hasSession(): + return opj(self.name, self.session) + else: + return self.name + + @property + def prefix(self): + """ The prefix to build filenames + + Returns: + A string 'sub-' or + 'sub-_ses-' + """ + if self.hasSession(): + return self.name + "_" + self.session + else: + return self.name + + def hasSession(self): + """ Check if a session is set + + Returns: + Boolean + """ + return self.session.strip() != DEFAULT.session \ No newline at end of file diff --git a/dcm2bids/scaffold/CHANGES b/dcm2bids/scaffold/CHANGES deleted file mode 100644 index 1c7518e8..00000000 --- a/dcm2bids/scaffold/CHANGES +++ /dev/null @@ -1,4 +0,0 @@ -Revision history for your dataset - -1.0.0 {} - - Initialized study directory diff --git a/dcm2bids/scaffold/README b/dcm2bids/scaffold/README deleted file mode 100644 index 80f71cd5..00000000 --- a/dcm2bids/scaffold/README +++ /dev/null @@ -1,33 +0,0 @@ -You should replace the content of this file and describe your dataset. - ---- - -BIDS Specification: https://bids-specification.readthedocs.io/en/stable/ - ---- - -BIDS directory overview -https://bids-specification.readthedocs.io/en/stable/02-common-principles.html#single-session-example - ---- - -participants.tsv and participants.json -https://bids-specification.readthedocs.io/en/stable/02-common-principles.html#tabular-files -https://bids-specification.readthedocs.io/en/stable/03-modality-agnostic-files.html#participants-file - ---- - -dataset_description.json -https://bids-specification.readthedocs.io/en/stable/03-modality-agnostic-files.html#dataset_description - ---- - -README -https://bids-specification.readthedocs.io/en/stable/03-modality-agnostic-files.html#readme - ---- - -CHANGES -https://bids-specification.readthedocs.io/en/stable/03-modality-agnostic-files.html#changes - ---- diff --git a/dcm2bids/scaffold/__init__.py b/dcm2bids/scaffold/__init__.py deleted file mode 100644 index c0348828..00000000 --- a/dcm2bids/scaffold/__init__.py +++ /dev/null @@ -1,62 +0,0 @@ -"""scaffold module""" - - -import sys -import argparse -import datetime -import os -import shutil -import importlib.resources as resources -from typing import Optional -from ..utils import write_txt - - -def _get_arguments(): - """Load arguments for main""" - parser = argparse.ArgumentParser( - formatter_class=argparse.RawDescriptionHelpFormatter, - description=""" - Create basic BIDS files and directories - """, - epilog=""" - Documentation at https://github.com/unfmontreal/Dcm2Bids - """, - ) - - parser.add_argument( - "-o", - "--output_dir", - required=False, - default=os.getcwd(), - help="Output BIDS directory, Default: current directory", - ) - - args = parser.parse_args() - return args - - -def scaffold(output_dir_override: Optional[str] = None): - """scaffold entry point""" - args = _get_arguments() - output_dir_ = output_dir_override if output_dir_override is not None else args.output_dir - - for _ in ["code", "derivatives", "sourcedata"]: - os.makedirs(os.path.join(output_dir_, _), exist_ok=True) - - for _ in [ - "dataset_description.json", - "participants.json", - "participants.tsv", - "README", - ]: - dest = os.path.join(output_dir_, _) - with resources.path(__name__, _) as src: - shutil.copyfile(src, dest) - - with resources.path(__name__, "CHANGES") as changes_template: - with open(changes_template) as _: - data = _.read().format(datetime.date.today().strftime("%Y-%m-%d")) - write_txt( - os.path.join(output_dir_, "CHANGES"), - data.split("\n")[:-1], - ) diff --git a/dcm2bids/scaffold/dataset_description.json b/dcm2bids/scaffold/dataset_description.json deleted file mode 100644 index 1db440f4..00000000 --- a/dcm2bids/scaffold/dataset_description.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "Name": "", - "BIDSVersion": "1.2.1", - "License": "", - "Authors": [ - "" - ], - "Acknowledgments": "", - "HowToAcknowledge": "", - "Funding": [ - "" - ], - "ReferencesAndLinks": [ - "" - ], - "DatasetDOI": "" -} diff --git a/dcm2bids/scaffold/participants.json b/dcm2bids/scaffold/participants.json deleted file mode 100644 index 3f4c5238..00000000 --- a/dcm2bids/scaffold/participants.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "education": { - "LongName": "Education level", - "Description": "Education level, self-rated by participant", - "Levels": { - "1": "Finished primary school", - "2": "Finished secondary school", - "3": "Student at university", - "4": "Has degree from university" - } - }, - "bmi": { - "LongName": "Body mass index", - "Units": "kilograms per squared meters", - "TermURL": "http://purl.bioontology.org/ontology/SNOMEDCT/60621009" - } -} \ No newline at end of file diff --git a/dcm2bids/scaffold/participants.tsv b/dcm2bids/scaffold/participants.tsv deleted file mode 100644 index 49f4f06f..00000000 --- a/dcm2bids/scaffold/participants.tsv +++ /dev/null @@ -1,4 +0,0 @@ -participant_id age sex group -sub-control01 34 M control -sub-control02 12 F control -sub-patient01 33 F patient diff --git a/dcm2bids/sidecar.py b/dcm2bids/sidecar.py index bb99d4b3..d2a58d6a 100644 --- a/dcm2bids/sidecar.py +++ b/dcm2bids/sidecar.py @@ -8,9 +8,10 @@ import re from collections import defaultdict, OrderedDict from fnmatch import fnmatch -from future.utils import iteritems -from .structure import Acquisition -from .utils import DEFAULT, load_json, splitext_ + +from dcm2bids.acquisition import Acquisition +from dcm2bids.utils.utils import DEFAULT, splitext_ +from dcm2bids.utils.io import load_json class Sidecar(object): @@ -189,7 +190,7 @@ def compare(name, pattern): result = [] - for tag, pattern in iteritems(criteria): + for tag, pattern in criteria.items(): name = data.get(tag, '') if isinstance(name, list): @@ -217,7 +218,7 @@ def build_acquisitions(self, participant): acquisitions_intendedFor = [] self.logger.info("Sidecars pairing:") - for sidecar, valid_descriptions in iteritems(self.graph): + for sidecar, valid_descriptions in self.graph.items(): sidecarName = os.path.basename(sidecar.root) # only one description for the sidecar @@ -273,7 +274,7 @@ def duplicates(seq): for i, item in enumerate(seq): tally[item].append(i) - for key, locs in iteritems(tally): + for key, locs in tally.items(): if len(locs) > 1: yield key, locs diff --git a/dcm2bids/utils.py b/dcm2bids/utils.py deleted file mode 100644 index d487157f..00000000 --- a/dcm2bids/utils.py +++ /dev/null @@ -1,190 +0,0 @@ -# -*- coding: utf-8 -*- - - -import csv -import json -import logging -import os -from pathlib import Path -import re -from collections import OrderedDict -import shlex -import shutil -from subprocess import check_output - - -class DEFAULT(object): - """ Default values of the package""" - - # cli dcm2bids - cliLogLevel = "INFO" - EPILOG="Documentation at https://github.com/unfmontreal/Dcm2Bids" - - # dcm2bids.py - outputDir = Path.cwd() - session = "" # also Participant object - clobber = False - forceDcm2niix = False - defaceTpl = None - logLevel = "WARNING" - - # dcm2niix.py - dcm2niixOptions = "-b y -ba y -z y -f '%3s_%f_%p_%t'" - dcm2niixVersion = "v1.0.20181125" - - # sidecar.py - compKeys = ["SeriesNumber", "AcquisitionTime", "SidecarFilename"] - searchMethod = "fnmatch" - searchMethodChoices = ["fnmatch", "re"] - runTpl = "_run-{:02d}" - caseSensitive = True - - # Entity table: - # https://bids-specification.readthedocs.io/en/v1.7.0/99-appendices/04-entity-table.html - entityTableKeys = ["sub", "ses", "task", "acq", "ce", "rec", "dir", - "run", "mod", "echo", "flip", "inv", "mt", "part", - "recording"] - - # misc - tmpDirName = "tmp_dcm2bids" - helperDir = "helper" - - -def load_json(filename): - """ Load a JSON file - - Args: - filename (str): Path of a JSON file - - Return: - Dictionnary of the JSON file - """ - with open(filename, "r") as f: - data = json.load(f, object_pairs_hook=OrderedDict) - return data - - -def save_json(filename, data): - with filename.open("w") as f: - json.dump(data, f, indent=4) - - -def write_txt(filename, lines): - with open(filename, "a") as f: - for row in lines: - f.write("%s\n" % row) - - -def write_participants(filename, participants): - with open(filename, "w") as f: - writer = csv.DictWriter(f, delimiter="\t", fieldnames=participants[0].keys()) - writer.writeheader() - writer.writerows(participants) - - -def read_participants(filename): - if not os.path.exists(filename): - return [] - with open(filename, "r") as f: - reader = csv.DictReader(f, delimiter="\t") - return [row for row in reader] - - -def splitext_(path, extensions=None): - """ Split the extension from a pathname - Handle case with extensions with '.' in it - - Args: - path (str): A path to split - extensions (list): List of special extensions - - Returns: - (root, ext): ext may be empty - """ - if extensions is None: - extensions = [".nii.gz"] - - for ext in extensions: - if path.endswith(ext): - return path[: -len(ext)], path[-len(ext) :] - return os.path.splitext(path) - - -def run_shell_command(commandLine): - """ Wrapper of subprocess.check_output - Returns: - Run command with arguments and return its output - """ - logger = logging.getLogger(__name__) - logger.info("Running %s", commandLine) - return check_output(commandLine) - - -def valid_path(in_path, type="folder"): - """Assert that file exists. - - Parameters - ---------- - required_file: Path - Path to be checked. - """ - if isinstance(in_path, str): - in_path = Path(in_path) - - if type == 'folder': - if in_path.is_dir() or in_path.parent.is_dir(): - return in_path - else: - raise NotADirectoryError(in_path) - elif type == "file": - if in_path.is_file(): - return in_path - else: - raise FileNotFoundError(in_path) - - raise TypeError(type) - -def assert_dirs_empty(parser, args, required): - """ - Assert that all directories exist are empty. - If dirs exist and not empty, and --force is used, delete dirs. - - Parameters - ---------- - parser: argparse.ArgumentParser object - Parser. - args: argparse namespace - Argument list. - required: string or list of paths to files - Required paths to be checked. - create_dir: bool - If true, create the directory if it does not exist. - """ - def check(path): - if not path.is_dir(): - return - - if not any(path.iterdir()): - return - - if not args.overwrite: - parser.error( - f"Output directory {path} isn't empty, so some files " - "could be overwritten or deleted.\nRerun the command with " - "--force option to overwrite existing output files.") - else: - for the_file in path.iterdir(): - file_path = path / the_file - try: - if file_path.is_file(): - file_path.unlink() - elif file_path.is_dir(): - shutil.rmtree(file_path) - except Exception as e: - print(e) - - if isinstance(required, str) or isinstance(required, Path): - required = [Path(required)] - - for cur_dir in required: - check(cur_dir) diff --git a/dcm2bids/utils/__init__.py b/dcm2bids/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/dcm2bids/utils/args.py b/dcm2bids/utils/args.py new file mode 100644 index 00000000..153648a5 --- /dev/null +++ b/dcm2bids/utils/args.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- + +import os +import shutil + + +def assert_dirs_empty(parser, args, required): + """ + Assert that all directories exist are empty. + If dirs exist and not empty, and --force is used, delete dirs. + + Parameters + ---------- + parser: argparse.ArgumentParser object + Parser. + args: argparse namespace + Argument list. + required: string or list of paths to files + Required paths to be checked. + """ + def check(path): + if os.path.isdir(path): + if os.listdir(path): + if not args.overwrite: + parser.error( + f"Output directory {path} isn't empty, so some files " + "could be overwritten or deleted.\nRerun the command" + " with --force option to overwrite " + "existing output files.") + else: + for the_file in os.listdir(path): + file_path = os.path.join(path, the_file) + try: + if os.path.isfile(file_path): + os.unlink(file_path) + elif os.path.isdir(file_path): + shutil.rmtree(file_path) + except Exception as e: + print(e) + + if isinstance(required, str): + required = [required] + + for cur_dir in required: + check(cur_dir) + + +def add_overwrite_arg(parser): + parser.add_argument( + '--force', dest='overwrite', action='store_true', + help='Force overwriting of the output files.') diff --git a/dcm2bids/utils/io.py b/dcm2bids/utils/io.py new file mode 100644 index 00000000..f28ed339 --- /dev/null +++ b/dcm2bids/utils/io.py @@ -0,0 +1,81 @@ +# -*- coding: utf-8 -*- + +import inspect +import json +import os +import os.path as opj +from pathlib import Path +from collections import OrderedDict + +import dcm2bids + + +def load_json(filename): + """ Load a JSON file + + Args: + filename (str): Path of a JSON file + + Return: + Dictionnary of the JSON file + """ + with open(filename, "r") as f: + data = json.load(f, object_pairs_hook=OrderedDict) + return data + + +def save_json(filename, data): + with open(filename, "w") as f: + json.dump(data, f, indent=4) + + +def write_txt(filename, lines): + with open(filename, "a+") as f: + f.write(f"{lines}\n") + + +def get_scaffold_dir(): + """ + Return SCAFFOLD data directory in dcm2bids repository + + Returns + ------- + scaffold_dir: string + SCAFFOLD path + """ + + module_path = os.path.dirname(os.path.dirname(inspect.getfile(dcm2bids))) + # module_path = inspect.getfile(dcm2bids) + scaffold_dir = opj(module_path, 'data', 'scaffold') + # scaffold_dir = pkg_resources.resource_filename(__name__, os.path.join("data", "scaffold")) + # print(module_path) + # scaffold_dir = os.path.join(os.path.dirname( + # os.path.dirname(module_path)), "data", "scaffold") + + print(scaffold_dir) + return scaffold_dir + + +def valid_path(in_path, type="folder"): + """Assert that file exists. + + Parameters + ---------- + required_file: Path + Path to be checked. + """ + if isinstance(in_path, str): + in_path = Path(in_path) + + if type == 'folder': + if in_path.is_dir() or in_path.parent.is_dir(): + return in_path + else: + raise NotADirectoryError(in_path) + elif type == "file": + if in_path.is_file(): + return in_path + else: + raise FileNotFoundError(in_path) + + raise TypeError(type) diff --git a/dcm2bids/logger.py b/dcm2bids/utils/logger.py similarity index 100% rename from dcm2bids/logger.py rename to dcm2bids/utils/logger.py diff --git a/dcm2bids/utils/scaffold.py b/dcm2bids/utils/scaffold.py new file mode 100644 index 00000000..70cc2e19 --- /dev/null +++ b/dcm2bids/utils/scaffold.py @@ -0,0 +1,222 @@ +# -*- coding: utf-8 -*- + +class bids_starter_kit(object): + + CHANGES = """Revision history for your dataset + +1.0.0 DATE + - Initialized study directory + """ + + dataset_description = """{ + "Name": "", + "BIDSVersion": "BIDS_VERSION", + "License": "", + "Authors": [ + "" + ], + "Acknowledgments": "", + "HowToAcknowledge": "", + "Funding": [ + "" + ], + "ReferencesAndLinks": [ + "" + ], + "DatasetDOI": "" +} +""" + + participants_json = """{ + "age": { + "LongName": "", + "Description": "age of the participant", + "Levels": [], + "Units": "years", + "TermURL": "" + }, + "sex": { + "LongName": "", + "Description": "sex of the participant as reported by the participant", + "Levels": { + "M": "male", + "F": "female" + }, + "Units": "", + "TermURL": "" + }, + "group": { + "LongName": "", + "Description": "Group of the participant", + "Levels": { + "control": "Control", + "patient": "Patient" + }, + "Units": "", + "TermURL": "" + }, + + +} +""" + + participants_tsv = """participant_id age sex group +sub-01 34 M control +sub-02 12 F control +sub-03 33 F patient +""" + + README = """# README + +The README is usually the starting point for researchers using your data +and serves as a guidepost for users of your data. A clear and informative +README makes your data much more usable. + +In general you can include information in the README that is not captured by some other +files in the BIDS dataset (dataset_description.json, events.tsv, ...). + +It can also be useful to also include information that might already be +present in another file of the dataset but might be important for users to be aware of +before preprocessing or analysing the data. + +If the README gets too long you have the possibility to create a `/doc` folder +and add it to the `.bidsignore` file to make sure it is ignored by the BIDS validator. + +More info here: https://neurostars.org/t/where-in-a-bids-dataset-should-i-put-notes-about-individual-mri-acqusitions/17315/3 + +## Details related to access to the data + +- [ ] Data user agreement + +If the dataset requires a data user agreement, link to the relevant information. + +- [ ] Contact person + +Indicate the name and contact details (email and ORCID) of the person responsible for additional information. + +- [ ] Practical information to access the data + +If there is any special information related to access rights or +how to download the data make sure to include it. +For example, if the dataset was curated using datalad, +make sure to include the relevant section from the datalad handbook: +http://handbook.datalad.org/en/latest/basics/101-180-FAQ.html#how-can-i-help-others-get-started-with-a-shared-dataset + +## Overview + +- [ ] Project name (if relevant) + +- [ ] Year(s) that the project ran + +If no `scans.tsv` is included, this could at least cover when the data acquisition +starter and ended. Local time of day is particularly relevant to subject state. + +- [ ] Brief overview of the tasks in the experiment + +A paragraph giving an overview of the experiment. This should include the +goals or purpose and a discussion about how the experiment tries to achieve +these goals. + +- [ ] Description of the contents of the dataset + +An easy thing to add is the output of the bids-validator that describes what type of +data and the number of subject one can expect to find in the dataset. + +- [ ] Independent variables + +A brief discussion of condition variables (sometimes called contrasts +or independent variables) that were varied across the experiment. + +- [ ] Dependent variables + +A brief discussion of the response variables (sometimes called the +dependent variables) that were measured and or calculated to assess +the effects of varying the condition variables. This might also include +questionnaires administered to assess behavioral aspects of the experiment. + +- [ ] Control variables + +A brief discussion of the control variables --- that is what aspects +were explicitly controlled in this experiment. The control variables might +include subject pool, environmental conditions, set up, or other things +that were explicitly controlled. + +- [ ] Quality assessment of the data + +Provide a short summary of the quality of the data ideally with descriptive statistics if relevant +and with a link to more comprehensive description (like with MRIQC) if possible. + +## Methods + +### Subjects + +A brief sentence about the subject pool in this experiment. + +Remember that `Control` or `Patient` status should be defined in the `participants.tsv` +using a group column. + +- [ ] Information about the recruitment procedure +- [ ] Subject inclusion criteria (if relevant) +- [ ] Subject exclusion criteria (if relevant) + +### Apparatus + +A summary of the equipment and environment setup for the +experiment. For example, was the experiment performed in a shielded room +with the subject seated in a fixed position. + +### Initial setup + +A summary of what setup was performed when a subject arrived. + +### Task organization + +How the tasks were organized for a session. +This is particularly important because BIDS datasets usually have task data +separated into different files.) + +- [ ] Was task order counter-balanced? +- [ ] What other activities were interspersed between tasks? + +- [ ] In what order were the tasks and other activities performed? + +### Task details + +As much detail as possible about the task and the events that were recorded. + +### Additional data acquired + +A brief indication of data other than the +imaging data that was acquired as part of this experiment. In addition +to data from other modalities and behavioral data, this might include +questionnaires and surveys, swabs, and clinical information. Indicate +the availability of this data. + +This is especially relevant if the data are not included in a `phenotype` folder. +https://bids-specification.readthedocs.io/en/stable/03-modality-agnostic-files.html#phenotypic-and-assessment-data + +### Experimental location + +This should include any additional information regarding the +the geographical location and facility that cannot be included +in the relevant json files. + +### Missing data + +Mention something if some participants are missing some aspects of the data. +This can take the form of a processing log and/or abnormalities about the dataset. + +Some examples: + +- A brain lesion or defect only present in one participant +- Some experimental conditions missing on a given run for a participant because + of some technical issue. +- Any noticeable feature of the data for certain participants +- Differences (even slight) in protocol for certain participants. + +### Notes + +Any additional information or pointers to information that +might be helpful to users of the dataset. Include qualitative information +related to how the data acquisition went. +""" diff --git a/dcm2bids/utils/tools.py b/dcm2bids/utils/tools.py new file mode 100644 index 00000000..13fbad2a --- /dev/null +++ b/dcm2bids/utils/tools.py @@ -0,0 +1,149 @@ +# -*- coding: utf-8 -*- + +"""This module take care of the versioning""" + + +import logging +import shlex +from packaging import version +from subprocess import check_output, CalledProcessError, TimeoutExpired +from shutil import which + +from dcm2bids.version import __version__ + +logger = logging.getLogger(__name__) + + +def is_tool(name): + """ Check if a program is in PATH + + Args: + name (string): program name + Returns: + boolean + """ + return which(name) is not None + + +def check_github_latest(githubRepo, timeout=3): + """ Check the latest version of a github repository + + Args: + githubRepo (string): a github repository ("username/repository") + timeout (int): time in seconds + + Returns: + A string of the version + """ + url = f"https://github.com/{githubRepo}/releases/latest" + try: + output = check_output(shlex.split("curl -L --silent " + url), timeout=timeout) + except CalledProcessError: + logger.info(f"Checking latest version of {githubRepo} was not possible") + logger.debug(f"Error while 'curl --silent {url}'", exc_info=True) + return + except TimeoutExpired: + logger.info(f"Checking latest version of {githubRepo} was not possible") + logger.debug(f"Command 'curl --silent {url}' timed out after {timeout}s") + return + # The output should have this format + # You are being redirected. + try: + version = output.decode().split(f"{githubRepo}/releases/tag/")[1].split('"')[0] + + # Versions are X.X.X + if len(version) > 5: + version = version[:5] + return version + except: + logger.debug( + "Checking latest version of %s was not possible", githubRepo, + exc_info=True, + ) + return + + +def check_latest(name="dcm2bids"): + """ Check if a new version of a software exists and print some details + Implemented for dcm2bids, dcm2niix + + Args: + name (string): name of the software + + Returns: + None + """ + data = { + "dcm2bids": { + "repo": "unfmontreal/Dcm2Bids", + "host": "https://github.com", + "current": __version__, + }, + "dcm2niix": { + "repo": "rordenlab/dcm2niix", + "host": "https://github.com", + "current": dcm2niix_version, + }, + } + + if is_tool("curl"): + host = data.get(name)["host"] + + if host == "https://github.com": + repo = data.get(name)["repo"] + latest = check_github_latest(repo) + + else: + # Not implemented + return False + + else: + logger.debug("Checking latest version of %s was not possible", name) + logger.debug("curl: %s", is_tool("curl")) + return + + current = data.get(name)["current"] + if callable(current): + current = current() + + try: + news = version(latest) > version(current) + except: + news = None + + if news: + logger.warning("Your using %s version %s", name, current) + logger.warning("A new version exists : %s", latest) + logger.warning("Check %s/%s", host, repo) + + +def dcm2niix_version(): + """ + Returns: + A string of the version of dcm2niix install on the system + """ + if not is_tool("dcm2niix"): + logger.error("dcm2niix is not in your PATH or not installed") + logger.error("Check https://github.com/rordenlab/dcm2niix") + return + + try: + output = check_output(shlex.split("dcm2niix")) + except: + logger.error("Running: dcm2niix", exc_info=True) + return + + try: + lines = output.decode().split("\n") + except: + logger.debug(output, exc_info=True) + return + + for line in lines: + try: + splits = line.split() + return splits[splits.index("version") + 1] + except: + continue + + return diff --git a/dcm2bids/utils/utils.py b/dcm2bids/utils/utils.py new file mode 100644 index 00000000..f2548c98 --- /dev/null +++ b/dcm2bids/utils/utils.py @@ -0,0 +1,95 @@ +# -*- coding: utf-8 -*- + + +import csv +import logging +import os +from pathlib import Path +from subprocess import check_output + + +class DEFAULT(object): + """ Default values of the package""" + + doc = "Documentation at https://unfmontreal.github.io/Dcm2Bids/" + + # cli dcm2bids + cliSession = "" + cliOutputDir = os.getcwd() + cliLogLevel = "INFO" + + # dcm2bids.py + outputDir = Path.cwd() + session = "" # also Participant object + clobber = False + forceDcm2niix = False + defaceTpl = None + logLevel = "WARNING" + + # dcm2niix.py + dcm2niixOptions = "-b y -ba y -z y -f '%3s_%f_%p_%t'" + + # sidecar.py + compKeys = ["SeriesNumber", "AcquisitionTime", "SidecarFilename"] + searchMethod = "fnmatch" + searchMethodChoices = ["fnmatch", "re"] + runTpl = "_run-{:02d}" + caseSensitive = True + + # Entity table: + # https://bids-specification.readthedocs.io/en/v1.7.0/99-appendices/04-entity-table.html + entityTableKeys = ["sub", "ses", "task", "acq", "ce", "rec", "dir", + "run", "mod", "echo", "flip", "inv", "mt", "part", + "recording"] + + # misc + tmpDirName = "tmp_dcm2bids" + helperDir = "helper" + + # BIDS version + bids_version = "v1.8.0" + + +def write_participants(filename, participants): + with open(filename, "w") as f: + writer = csv.DictWriter(f, delimiter="\t", fieldnames=participants[0].keys()) + writer.writeheader() + writer.writerows(participants) + + +def read_participants(filename): + if not os.path.exists(filename): + return [] + with open(filename, "r") as f: + reader = csv.DictReader(f, delimiter="\t") + return [row for row in reader] + + +def splitext_(path, extensions=None): + """ Split the extension from a pathname + Handle case with extensions with '.' in it + + Args: + path (str): A path to split + extensions (list): List of special extensions + + Returns: + (root, ext): ext may be empty + """ + if extensions is None: + extensions = [".nii.gz"] + + for ext in extensions: + if path.endswith(ext): + return path[: -len(ext)], path[-len(ext) :] + return os.path.splitext(path) + + +def run_shell_command(commandLine): + """ Wrapper of subprocess.check_output + Returns: + Run command with arguments and return its output + """ + logger = logging.getLogger(__name__) + logger.info("Running %s", commandLine) + return check_output(commandLine) diff --git a/dcm2bids/version.py b/dcm2bids/version.py index 1f4d7130..d7074903 100644 --- a/dcm2bids/version.py +++ b/dcm2bids/version.py @@ -1,151 +1,54 @@ # -*- coding: utf-8 -*- -"""This module take care of the versioning""" - -# dcm2bids version -__version__ = "2.1.9" - - -import logging -import shlex -from distutils.version import LooseVersion -from subprocess import check_output, CalledProcessError, TimeoutExpired -from shutil import which - - -logger = logging.getLogger(__name__) - - -def is_tool(name): - """ Check if a program is in PATH - - Args: - name (string): program name - Returns: - boolean - """ - return which(name) is not None - - -def check_github_latest(githubRepo, timeout=3): - """ Check the latest version of a github repository - - Args: - githubRepo (string): a github repository ("username/repository") - timeout (int): time in seconds - - Returns: - A string of the version - """ - url = "https://github.com/{}/releases/latest".format(githubRepo) - try: - output = check_output(shlex.split("curl -L --silent " + url), timeout=timeout) - except CalledProcessError: - logger.info(f"Checking latest version of {githubRepo} was not possible") - logger.debug(f"Error while 'curl --silent {url}'", exc_info=True) - return - except TimeoutExpired: - logger.info(f"Checking latest version of {githubRepo} was not possible") - logger.debug(f"Command 'curl --silent {url}' timed out after {timeout}s") - return - # The output should have this format - # You are being redirected. - try: - version = output.decode().split("{}/releases/tag/".format(githubRepo))[1].split('"')[0] - - # Versions are X.X.X - if len(version) > 5: - version = version[:5] - return version - except: - logger.debug( - "Checking latest version of %s was not possible", githubRepo, - exc_info=True, - ) - return - - -def check_latest(name="dcm2bids"): - """ Check if a new version of a software exists and print some details - Implemented for dcm2bids, dcm2niix - - Args: - name (string): name of the software - - Returns: - None - """ - data = { - "dcm2bids": { - "repo": "unfmontreal/Dcm2Bids", - "host": "https://github.com", - "current": __version__, - }, - "dcm2niix": { - "repo": "rordenlab/dcm2niix", - "host": "https://github.com", - "current": dcm2niix_version, - }, - } - - if is_tool("curl"): - host = data.get(name)["host"] - - if host == "https://github.com": - repo = data.get(name)["repo"] - latest = check_github_latest(repo) - - else: - # Not implemented - return - - else: - logger.debug("Checking latest version of %s was not possible", name) - logger.debug("curl: %s", is_tool("curl")) - return - - current = data.get(name)["current"] - if callable(current): - current = current() - - try: - news = LooseVersion(latest) > LooseVersion(current) - except: - news = None - - if news: - logger.warning("Your using %s version %s", name, current) - logger.warning("A new version exists : %s", latest) - logger.warning("Check %s/%s", host, repo) - - -def dcm2niix_version(): - """ - Returns: - A string of the version of dcm2niix install on the system - """ - if not is_tool("dcm2niix"): - logger.error("dcm2niix is not in your PATH or not installed") - logger.error("Check https://github.com/rordenlab/dcm2niix") - return - - try: - output = check_output(shlex.split("dcm2niix")) - except: - logger.error("Running: dcm2niix", exc_info=True) - return - - try: - lines = output.decode().split("\n") - except: - logger.debug(output, exc_info=True) - return - - for line in lines: - try: - splits = line.split() - return splits[splits.index("version") + 1] - except: - continue - - return +# Format expected by setup.py and doc/source/conf.py: string of form "X.Y.Z" +_version_major = 3 +_version_minor = 0 +_version_micro = 0 +_version_extra = 'dev' + +# Construct full version string from these. +_ver = [_version_major, _version_minor] +if _version_micro: + _ver.append(_version_micro) +if _version_extra: + _ver.append(_version_extra) + +__version__ = '.'.join(map(str, _ver)) + +CLASSIFIERS = [ + "Intended Audience :: Healthcare Industry", + "Intended Audience :: Science/Research", + "Operating System :: MacOS", + "Operating System :: Microsoft :: Windows", + "Operating System :: Unix", + "Programming Language :: Python", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Topic :: Scientific/Engineering", + "Topic :: Scientific/Engineering :: Bio-Informatics", + "Topic :: Scientific/Engineering :: Medical Science Apps.", +] + +# Description should be a one-liner: +description = "Reorganising NIfTI files from dcm2niix into the Brain Imaging Data Structure" + +NAME = "dcm2bids" +MAINTAINER = "Arnaud Boré" +MAINTAINER_EMAIL = "arnaud.bore@gmail.com" +DESCRIPTION = description +PROJECT_URLS = { + "Documentation": "https://unfmontreal.github.io/Dcm2Bids", + "Source Code": "https://github.com/unfmontreal/Dcm2Bids", +} +LICENSE = "GPLv3+" +PLATFORMS = "OS Independent" +MAJOR = _version_major +MINOR = _version_minor +MICRO = _version_micro +VERSION = __version__ +ENTRY_POINTS = {'console_scripts': [ + 'dcm2bids=dcm2bids.cli.dcm2bids:main', + 'dcm2bids_helper=dcm2bids.cli.dcm2bids_helper:main', + 'dcm2bids_scaffold=dcm2bids.cli.dcm2bids_scaffold:main', +]} diff --git a/requirements-doc.txt b/requirements-doc.txt index 9737dbd2..ba3eff87 100644 --- a/requirements-doc.txt +++ b/requirements-doc.txt @@ -1,7 +1,7 @@ asttokens==2.0.5 Babel==2.9.1 backcall==0.2.0 -certifi==2021.10.8 +certifi==2022.12.7 charset-normalizer==2.0.12 click==8.1.2 decorator==5.1.1 @@ -10,18 +10,18 @@ docstring-parser==0.7.3 executing==0.8.3 falcon==2.0.0 filelock==3.6.0 -future==0.18.2 +future==0.18.3 ghp-import==2.0.2 gitdb==4.0.9 -GitPython==3.1.27 +GitPython==3.1.29 hug==2.6.1 idna==3.3 importlib-metadata==4.11.3 -ipython==8.2.0 +ipython==8.10.0 jedi==0.18.1 Jinja2==3.1.1 livereload==2.6.3 -Mako==1.2.0 +Mako==1.2.2 Markdown==3.3.6 MarkupSafe==2.1.1 matplotlib-inline==0.1.3 @@ -41,7 +41,7 @@ pickleshare==0.7.5 platformdirs==2.5.1 pluggy==1.0.0 portray==1.7.0 -prompt-toolkit==3.0.29 +prompt-toolkit==3.0.30 ptyprocess==0.7.0 pure-eval==0.2.2 py==1.11.0 diff --git a/requirements-test.txt b/requirements-test.txt index be0928aa..e7557a46 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -4,3 +4,4 @@ pytest-black>=0.3.7 pytest-cov>=2.8.1 pytest-flake8>=1.0.4 pytest-pylint>=0.14.1 +pytest-console-scripts==1.4.1 \ No newline at end of file diff --git a/setup.py b/setup.py index a252cae0..5ceac7f3 100755 --- a/setup.py +++ b/setup.py @@ -1,87 +1,35 @@ -#!/usr/bin/env python # -*- coding: utf-8 -*- -# type: ignore -# pylint: disable=exec-used -"""Setup file for the dcm2bids package""" - -import os +from os.path import join as opj +from os import path from setuptools import setup, find_packages +# Get version and release info, which is all stored in dcm2bids/version.py +ver_file = opj('dcm2bids', 'version.py') +with open(ver_file) as f: + exec(f.read()) -def load_version(): - """Execute dcm2bids.version in a global dictionary""" - global_dict = {} - with open(os.path.join("dcm2bids", "version.py")) as _: - exec(_.read(), global_dict) - return global_dict - - -def install_requires(): - """Get list of required modules""" - required = [] - for module, meta in _VERSION["REQUIRED_MODULE_METADATA"]: - required.append("{}>={}".format(module, meta["min_version"])) - return required +# Long description will go up on the pypi page +here = path.abspath(path.dirname(__file__)) - -_VERSION = load_version() -DISTNAME = "dcm2bids" -VERSION = _VERSION["__version__"] -ENTRY_POINTS = { - "console_scripts": [ - "dcm2bids = dcm2bids.dcm2bids:main", - "dcm2bids_helper = dcm2bids.helper:main", - "dcm2bids_scaffold = dcm2bids:scaffold", - ], - # "configurations": [], -} -AUTHOR = "Christophe Bedetti" -AUTHOR_EMAIL = "christophe.bedetti@umontreal.ca" -DESCRIPTION = ( - "Reorganising NIfTI files from dcm2niix into the Brain Imaging Data Structure" -) -with open("README.md", encoding="utf-8") as _: +with open(opj(here, "README.md"), encoding="utf-8") as _: LONG_DESCRIPTION = _.read() -LICENSE = "GPLv3+" -PROJECT_URLS = { - "Documentation": "https://unfmontreal.github.io/Dcm2Bids", - "Source Code": "https://github.com/unfmontreal/Dcm2Bids", -} -CLASSIFIERS = [ - "Intended Audience :: Healthcare Industry", - "Intended Audience :: Science/Research", - "Operating System :: MacOS", - "Operating System :: Microsoft :: Windows", - "Operating System :: Unix", - "Programming Language :: Python", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Topic :: Scientific/Engineering", - "Topic :: Scientific/Engineering :: Bio-Informatics", - "Topic :: Scientific/Engineering :: Medical Science Apps.", -] - -if __name__ == "__main__": - setup( - name=DISTNAME, - version=VERSION, - packages=find_packages(exclude=["tests"]), - entry_points=ENTRY_POINTS, - python_requires=">=3.7", - use_scm_version=True, - setup_requires=['setuptools_scm'], - install_requires=['future>=0.17.1'], - include_package_data=True, - author=AUTHOR, - author_email=AUTHOR_EMAIL, - description=DESCRIPTION, - long_description=LONG_DESCRIPTION, - long_description_content_type="text/markdown", - # keywords="", - license=LICENSE, - project_urls=PROJECT_URLS, - classifiers=CLASSIFIERS, - ) +opts = dict(name=NAME, + maintainer=MAINTAINER, + maintainer_email=MAINTAINER_EMAIL, + description=DESCRIPTION, + long_description=LONG_DESCRIPTION, + long_description_content_type="text/markdown", + project_urls=PROJECT_URLS, + license=LICENSE, + classifiers=CLASSIFIERS, + platforms=PLATFORMS, + python_requires=">3.7", + install_requires=['packaging>=23.1'], + version=VERSION, + packages=find_packages(exclude=["tests"]), + entry_points=ENTRY_POINTS) + + +setup(**opts) diff --git a/tests/data/config_test.json b/tests/data/config_test.json index f0acddec..7220ba8d 100644 --- a/tests/data/config_test.json +++ b/tests/data/config_test.json @@ -42,7 +42,7 @@ "EchoNumber": 1, "EchoTime": 0.00492 }, - "intendedFor": 3 + "intendedFor": [3,2] }, { "dataType": "fmap", diff --git a/tests/test_dcm2bids.py b/tests/test_dcm2bids.py index d20d3b34..07799d08 100644 --- a/tests/test_dcm2bids.py +++ b/tests/test_dcm2bids.py @@ -3,18 +3,23 @@ import json import os +import pytest import shutil from tempfile import TemporaryDirectory from bids import BIDSLayout -from dcm2bids import Dcm2bids -from dcm2bids.utils import DEFAULT, load_json - +from dcm2bids.dcm2bids_gen import Dcm2BidsGen +from dcm2bids.utils.utils import DEFAULT +from dcm2bids.utils.io import load_json TEST_DATA_DIR = os.path.join(os.path.dirname(__file__), "data") +def test_help_option(script_runner): + ret = script_runner.run(['dcm2bids', '--help']) + assert ret.success + def compare_json(original_file, converted_file): with open(original_file) as f: original_json = json.load(f) @@ -35,7 +40,7 @@ def test_dcm2bids(): tmpSubDir = os.path.join(bidsDir.name, DEFAULT.tmpDirName, "sub-01") shutil.copytree(os.path.join(TEST_DATA_DIR, "sidecars"), tmpSubDir) - app = Dcm2bids( + app = Dcm2BidsGen( [TEST_DATA_DIR], "01", os.path.join(TEST_DATA_DIR, "config_test.json"), @@ -49,13 +54,18 @@ def test_dcm2bids(): assert layout.get_tasks() == ["rest"] assert layout.get_runs() == [1, 2, 3] - app = Dcm2bids(TEST_DATA_DIR, "01", + app = Dcm2BidsGen(TEST_DATA_DIR, "01", os.path.join(TEST_DATA_DIR, "config_test.json"), bidsDir.name) app.run() fmapFile = os.path.join(bidsDir.name, "sub-01", "fmap", "sub-01_echo-492_fmap.json") data = load_json(fmapFile) + assert data["IntendedFor"] == [os.path.join("dwi", "sub-01_dwi.nii.gz"), + os.path.join("anat", "sub-01_T1w.nii.gz")] + + fmapFile = os.path.join(bidsDir.name, "sub-01", "fmap", "sub-01_echo-738_fmap.json") + data = load_json(fmapFile) fmapMtime = os.stat(fmapFile).st_mtime assert data["IntendedFor"] == os.path.join("dwi", "sub-01_dwi.nii.gz") @@ -70,7 +80,7 @@ def test_dcm2bids(): shutil.rmtree(tmpSubDir) shutil.copytree(os.path.join(TEST_DATA_DIR, "sidecars"), tmpSubDir) - app = Dcm2bids( + app = Dcm2BidsGen( [TEST_DATA_DIR], "01", os.path.join(TEST_DATA_DIR, "config_test.json"), @@ -89,7 +99,7 @@ def test_caseSensitive_false(): tmpSubDir = os.path.join(bidsDir.name, DEFAULT.tmpDirName, "sub-01") shutil.copytree(os.path.join(TEST_DATA_DIR, "sidecars"), tmpSubDir) - app = Dcm2bids(TEST_DATA_DIR, "01", + app = Dcm2BidsGen(TEST_DATA_DIR, "01", os.path.join(TEST_DATA_DIR, "config_test_not_case_sensitive_option.json"), bidsDir.name) diff --git a/tests/test_dcm2niix.py b/tests/test_dcm2niix.py index 81b7a281..0aa83823 100644 --- a/tests/test_dcm2niix.py +++ b/tests/test_dcm2niix.py @@ -5,8 +5,8 @@ from tempfile import TemporaryDirectory import os import pytest -from dcm2bids.dcm2niix import Dcm2niix -from dcm2bids.utils import DEFAULT +from dcm2bids.dcm2niix_gen import Dcm2niixGen +from dcm2bids.utils.utils import DEFAULT TEST_DATA_DIR = os.path.join(os.path.dirname(__file__), "data") @@ -19,7 +19,7 @@ def test_dcm2niix_run(): # tmpDir = TemporaryDirectory(dir=tmpBase) tmpDir = TemporaryDirectory() - app = Dcm2niix([dicomDir], tmpDir.name) + app = Dcm2niixGen([dicomDir], tmpDir.name) app.run() helperDir = os.path.join(tmpDir.name, DEFAULT.tmpDirName, DEFAULT.helperDir, "*") diff --git a/tests/test_helper.py b/tests/test_helper.py new file mode 100644 index 00000000..8a4b3fba --- /dev/null +++ b/tests/test_helper.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- + +import pytest + + +def test_help_option(script_runner): + ret = script_runner.run(['dcm2bids_helper', '--help']) + assert ret.success diff --git a/tests/test_scaffold.py b/tests/test_scaffold.py new file mode 100644 index 00000000..f45d98df --- /dev/null +++ b/tests/test_scaffold.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- + +import pytest + + +def test_help_option(script_runner): + ret = script_runner.run(['dcm2bids_scaffold', '--help']) + assert ret.success + +def test_run_scaffold(script_runner): + ret = script_runner.run(['dcm2bids_scaffold', '-o', 'o_scaffold', '--force']) + assert ret.success diff --git a/tests/test_structure.py b/tests/test_structure.py index 6f374871..96fe8843 100644 --- a/tests/test_structure.py +++ b/tests/test_structure.py @@ -3,32 +3,37 @@ from os.path import join as opj import pytest -from dcm2bids.structure import Participant, Acquisition +from dcm2bids.participant import Participant +from dcm2bids.acquisition import Acquisition @pytest.mark.parametrize( "name,session,modality,custom,expected", [ ("AB", "", "T1w", "", opj("sub-AB", "anat", "sub-AB_T1w")), - ("sub-AB", " ", "_T1w", "run-03", opj("sub-AB", "anat", "sub-AB_run-03_T1w")), - ( - "sub-AB", - "01", - "_T1w", - " ", - opj("sub-AB", "ses-01", "anat", "sub-AB_ses-01_T1w"), - ), - ( - "sub-AB", - "ses-01", - "T1w", - "_run-03", - opj("sub-AB", "ses-01", "anat", "sub-AB_ses-01_run-03_T1w"), - ), + ("sub-AA", " ", "T1w_T2w", "new-test", opj("sub-AA", "anat", "sub-AA_new-test_T1w_T2w")), + ("sub-AB", " ", "_T1w", "run-03", opj("sub-AB", "anat", "sub-AB_run-03_T1w")), + ("sub-AB", "01", "_T1w", " ", opj("sub-AB", "ses-01", "anat", "sub-AB_ses-01_T1w")), + ("sub-AB", "ses-01", "T1w", "run-03", opj("sub-AB", "ses-01", "anat", "sub-AB_ses-01_run-03_T1w")), + ("sub-AB", "ses-01", "T1w", "run-04_rec-test_ce-test", opj("sub-AB", "ses-01", "anat", "sub-AB_ses-01_ce-test_rec-test_run-04_T1w")) ], ) + def test_acquisition_get_dst_path(name, session, modality, custom, expected): participant = Participant(name, session) acquisition = Acquisition(participant, "anat", modality, customLabels=custom) acquisition.setDstFile() assert acquisition.dstRoot == expected + +@pytest.mark.parametrize( + "name_c,session_c,modality_c,custom_c", + [ + ("AB", "", "_T1w", ""), + ], +) + +def test_comparison_acquisitions(name_c, session_c, modality_c, custom_c): + participant = Participant(name_c, session_c) + acquisition1 = Acquisition(participant, "anat", modality_c, customLabels=custom_c) + acquisition2 = Acquisition(participant, "anat", modality_c, customLabels=custom_c) + assert acquisition1 == acquisition2 \ No newline at end of file diff --git a/tests/test_version.py b/tests/test_version.py index 4ac8d361..6ae337cb 100644 --- a/tests/test_version.py +++ b/tests/test_version.py @@ -1,8 +1,13 @@ # -*- coding: utf-8 -*- -from dcm2bids.version import is_tool, check_github_latest, __version__ +from dcm2bids.utils.tools import is_tool def test_is_tool(): assert is_tool("dcm2bids") + + + +def test_dummy_tool(): + assert not is_tool("dummy_cmd") \ No newline at end of file diff --git a/tox.ini b/tox.ini index b0d10f6f..1b8e21c0 100644 --- a/tox.ini +++ b/tox.ini @@ -4,22 +4,22 @@ # and then run "tox" from this directory. [tox] -envlist = py37, py38, py39 -; envlist = clean, py37, py38, py39, report +envlist = py38, py39, py310 +; envlist = clean, py38, py39, py310, report [gh-actions] python = - 3.7: py37 3.8: py38 3.9: py39 + 3.10: py310 [testenv] deps = -rrequirements-test.txt commands = pytest --cov --cov-report=xml -s ; commands = pytest --cov --cov-append --black --flake8 --pylint ; depends = -; {py37, py38, py39}: clean -; report: py37, py38, py39 +; {py38, py39, py310}: clean +; report: py38, py39, py310 ; [testenv:report] ; deps = coverage