diff --git a/.github/workflows/run_tests_cli.yml b/.github/workflows/run_tests_cli.yml index a06883c98..a1a4a3988 100644 --- a/.github/workflows/run_tests_cli.yml +++ b/.github/workflows/run_tests_cli.yml @@ -18,6 +18,11 @@ jobs: # only trigger update on upstream repo if: github.repository_owner == 'cpp-lln-lab' + strategy: + fail-fast: false + matrix: + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] + steps: - name: Install dependencies @@ -34,8 +39,9 @@ jobs: node-version: 18 - uses: actions/setup-python@v5 + name: Set up Python ${{ matrix.python-version }} with: - python-version: '3.10' + python-version: ${{ matrix.python-version }} - name: Clone bidspm uses: actions/checkout@v4 @@ -46,7 +52,7 @@ jobs: - name: Install validators run: | make install - pip install .[dev] + pip install .[test] - name: Run tests and generate coverage report run: | @@ -61,4 +67,3 @@ jobs: flags: cli name: codecov-cli fail_ci_if_error: false - # token: ${{ secrets.CODECOV_TOKEN }} # not required but might help API rate limits diff --git a/Dockerfile_matlab b/WIP/Dockerfile_matlab similarity index 100% rename from Dockerfile_matlab rename to WIP/Dockerfile_matlab diff --git a/bidspm.m b/bidspm.m index 0ede8e4b5..f9a779d88 100644 --- a/bidspm.m +++ b/bidspm.m @@ -112,7 +112,7 @@ function displayArguments(varargin) end function value = rootDir() - value = fullfile(fileparts(mfilename('fullpath'))); + value = fileparts(mfilename('fullpath')); end %% low level actions diff --git a/demos/openneuro/ds002799_run.m b/demos/openneuro/ds002799_run.m index 87d003d29..a75f0d102 100644 --- a/demos/openneuro/ds002799_run.m +++ b/demos/openneuro/ds002799_run.m @@ -12,7 +12,7 @@ bidspm(); % The directory where the data are located -root_dir = fullfile(fileparts(mfilename('fullpath'))); +root_dir = fileparts(mfilename('fullpath')); root_dir = pwd; %% Parameters diff --git a/demos/scripts/inverse_normalize.m b/demos/scripts/inverse_normalize.m index 9ec4a686e..4edb0c9b0 100644 --- a/demos/scripts/inverse_normalize.m +++ b/demos/scripts/inverse_normalize.m @@ -1,6 +1,6 @@ % (C) Copyright 2022 bidspm developers -this_dir = fullfile(fileparts(mfilename('fullpath'))); +this_dir = fileparts(mfilename('fullpath')); % we are using the output data from the MoAE demo % so make sure you have run it before trying this. diff --git a/lib/CPP_ROI b/lib/CPP_ROI index ee40802b7..4b4f7f2b5 160000 --- a/lib/CPP_ROI +++ b/lib/CPP_ROI @@ -1 +1 @@ -Subproject commit ee40802b744ca4c8ff1aa88c4076f5d71de58706 +Subproject commit 4b4f7f2b5614fc2b30d807a2e05b73baa2a34c0f diff --git a/lib/bids-matlab b/lib/bids-matlab index 845b18662..5833faeb4 160000 --- a/lib/bids-matlab +++ b/lib/bids-matlab @@ -1 +1 @@ -Subproject commit 845b186629f421840d5cf43e197655b597cf4dd9 +Subproject commit 5833faeb4f236cccd472ad2281f2b97bb3710dd6 diff --git a/lib/spm_2_bids b/lib/spm_2_bids index bec6ef8fe..900627a43 160000 --- a/lib/spm_2_bids +++ b/lib/spm_2_bids @@ -1 +1 @@ -Subproject commit bec6ef8feb228bef05ebe1b85a2800e9189a9655 +Subproject commit 900627a4346dd6b61f5b47b9782f11919e8acf94 diff --git a/pyproject.toml b/pyproject.toml index ca1b41f3f..d0b703a4d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,10 +30,8 @@ demo = [ "octave-kernel" ] dev = [ - "bidspm[demo,doc,style]", + "bidspm[demo,doc,style,test]", "cffconvert", - "coverage", - "pytest", "ruamel.yaml", 'pandas' ] @@ -53,20 +51,17 @@ doc = [ ] docs = ["bidspm[doc]"] style = [ - "black", - "codespell", - "flake8", - "flake8-docstrings", - "miss-hit", - "mypy", "pre-commit", - "reorder-python-imports", - "rstcheck", "sourcery" ] +test = [ + "coverage", + "pytest" +] [project.scripts] -bidspm = "bidspm.bidspm:cli" +bidspm = "bidspm.cli:cli" +bidspm_2 = "bidspm.cli:new_cli" validate_model = "bidspm.validate:cli" [project.urls] diff --git a/src/bidspm/bidspm.py b/src/bidspm/bidspm.py index 8c0cb99f9..7e3eef7b2 100755 --- a/src/bidspm/bidspm.py +++ b/src/bidspm/bidspm.py @@ -2,23 +2,21 @@ from __future__ import annotations import subprocess -import sys from pathlib import Path from typing import Any from rich import print from .matlab import matlab -from .parsers import bidspm_log, common_parser +from .parsers import bidspm_log log = bidspm_log(name="bidspm") -new_line = ", ...\n\t " +new_line = ", ...\n\t " def base_cmd(bids_dir: Path, output_dir: Path) -> str: - cmd = " bidspm('init');" - cmd += f" bidspm( '{bids_dir}'{new_line}'{output_dir}'" + cmd = f" bidspm( '{bids_dir}'{new_line}'{output_dir}'" return cmd @@ -28,7 +26,7 @@ def append_main_cmd(cmd: str, analysis_level: str, action: str) -> str: def end_cmd(cmd: str) -> str: - cmd += "); exit;" + cmd += "); exit;" return cmd @@ -99,10 +97,6 @@ def default_model( ignore: list[str] | None = None, options: Path | None = None, ) -> int | str: - if space and len(space) > 1: - log.error(f"Only one space allowed for statistical analysis. Got\n:{space}") - return 1 - if task is None: task = "{''}" @@ -141,10 +135,6 @@ def preprocess( dry_run: bool = False, options: Path | None = None, ) -> int | str: - if action == "preprocess" and task and len(task) > 1: - log.error(f"Only one task allowed for preprocessing. Got\n:{task}") - return 1 - cmd = generate_cmd( bids_dir=bids_dir, output_dir=output_dir, @@ -229,8 +219,8 @@ def stats( output_dir: Path, analysis_level: str, action: str, - preproc_dir: Path, - model_file: Path, + preproc_dir: Path | None, + model_file: Path | None, verbosity: int = 2, participant_label: list[str] | None = None, fwhm: Any = 6, @@ -246,10 +236,6 @@ def stats( keep_residuals: bool = False, options: Path | None = None, ) -> int | str: - if space and len(space) > 1: - log.error(f"Only one space allowed for statistical analysis. Got\n:{space}") - return 1 - cmd = generate_cmd( bids_dir=bids_dir, output_dir=output_dir, @@ -281,7 +267,7 @@ def stats( cmd += f"{new_line}'keep_residuals', true" cmd = end_cmd(cmd) - log.info("Running statistics.") + log.info(f"Running {action}.") return cmd @@ -310,63 +296,6 @@ def generate_cmd( return cmd -def cli(argv: Any = sys.argv) -> None: - parser = common_parser() - - args, _ = parser.parse_known_args(argv[1:]) - - bids_dir = Path(args.bids_dir[0]).absolute() - output_dir = Path(args.output_dir[0]).absolute() - analysis_level = args.analysis_level[0] - action = args.action[0] - roi_atlas = args.roi_atlas[0] - bids_filter_file = ( - Path(args.bids_filter_file[0]).absolute() - if args.bids_filter_file is not None - else None - ) - preproc_dir = ( - Path(args.preproc_dir[0]).absolute() if args.preproc_dir is not None else None - ) - model_file = ( - Path(args.model_file[0]).absolute() if args.model_file is not None else None - ) - options = Path(args.options).absolute() if args.options is not None else None - - return_code = bidspm( - bids_dir, - output_dir, - analysis_level, - action=action, - participant_label=args.participant_label, - verbosity=args.verbosity, - task=args.task, - space=args.space, - ignore=args.ignore, - fwhm=args.fwhm, - bids_filter_file=bids_filter_file, - dummy_scans=args.dummy_scans, - anat_only=args.anat_only, - skip_validation=args.skip_validation, - dry_run=args.dry_run, - preproc_dir=preproc_dir, - model_file=model_file, - roi_based=args.roi_based, - roi_atlas=roi_atlas, - roi_name=args.roi_name, - roi_dir=args.roi_dir, - concatenate=args.concatenate, - design_only=args.design_only, - keep_residuals=args.keep_residuals, - options=options, - ) - - if return_code == 1: - sys.exit(1) - else: - sys.exit(0) - - def bidspm( bids_dir: Path, output_dir: Path, @@ -394,13 +323,6 @@ def bidspm( keep_residuals: bool = False, options: Path | None = None, ) -> int: - if not bids_dir.is_dir(): - log.error(f"The 'bids_dir' does not exist:\n\t{bids_dir}") - return 1 - - if preproc_dir is not None and not preproc_dir.is_dir(): - log.error(f"The 'preproc_dir' does not exist:\n\t{preproc_dir}") - return 1 if action == "default_model": cmd = default_model( @@ -448,14 +370,6 @@ def bidspm( ) elif action in {"stats", "contrasts", "results"}: - if preproc_dir is None or not preproc_dir.exists(): - log.error(f"'preproc_dir' must be specified for stats. Got:\n{preproc_dir}") - return 1 - - if model_file is None or not model_file.exists(): - log.error(f"'model_file' must be specified for stats. Got:\n{model_file}") - return 1 - cmd = stats( bids_dir=bids_dir, output_dir=output_dir, @@ -477,11 +391,13 @@ def bidspm( keep_residuals=keep_residuals, options=options, ) - else: - log.error(f"\nunknown action: {action}") - return 1 - return cmd if isinstance(cmd, int) else run_command(cmd) + if isinstance(cmd, int): + return cmd + + cmd = f" bidspm('init'); try; {cmd} catch; exit; end;" + + return run_command(cmd) def run_command(cmd: str, platform: str | None = None) -> int: diff --git a/src/bidspm/cli.py b/src/bidspm/cli.py new file mode 100644 index 000000000..19d50ffaf --- /dev/null +++ b/src/bidspm/cli.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import json +import sys +from pathlib import Path +from typing import Any + +from .bidspm import bidspm +from .parsers import ALLOWED_ACTIONS, bidspm_log, common_parser, sub_command_parser + +log = bidspm_log(name="bidspm") + +NOT_IMPLEMENTED = { + "bms", + "bms-bms", + "bms-posterior", + "copy", + "specify_only", +} + +with open(Path(__file__).parent / "data" / "exit_codes.json") as f: + EXIT_CODES = json.load(f) + +SUPPORTED_ACTIONS = set(ALLOWED_ACTIONS) - NOT_IMPLEMENTED + + +ul_it = "\n\t- " + + +def validate_actions(action: str) -> None: + if action not in ALLOWED_ACTIONS: + log.error( + f"Unknown action: '{action}'." + "\nSupported actions are:" + f"{ul_it}{ul_it.join(SUPPORTED_ACTIONS)}" + ) + raise SystemExit(EXIT_CODES["USAGE"]["Value"]) + + if action in NOT_IMPLEMENTED: + log.error( + f"The action '{action}' is not yet implemented." + "\nSupported actions are:" + f"{ul_it}{ul_it.join(SUPPORTED_ACTIONS)}" + ) + raise SystemExit(EXIT_CODES["USAGE"]["Value"]) + + +def cli(argv: Any = sys.argv) -> None: + parser = common_parser() + + args, _ = parser.parse_known_args(argv[1:]) + + bids_dir = Path(args.bids_dir[0]).absolute() + output_dir = Path(args.output_dir[0]).absolute() + analysis_level = args.analysis_level[0] + action = args.action[0] + roi_atlas = args.roi_atlas[0] + bids_filter_file = ( + Path(args.bids_filter_file[0]).absolute() + if args.bids_filter_file is not None + else None + ) + + preproc_dir: Path | None = ( + Path(args.preproc_dir[0]).absolute() if args.preproc_dir is not None else None + ) + model_file: Path | None = ( + Path(args.model_file[0]).absolute() if args.model_file is not None else None + ) + options: Path | None = ( + Path(args.options).absolute() if args.options is not None else None + ) + + space = args.space + + task = args.task + + validate_actions(action) + + if not bids_dir.is_dir(): + log.error("The 'bids_dir' does not exist:\n\t" f"{bids_dir}") + exit(EXIT_CODES["NOINPUT"]["Value"]) + + if preproc_dir is not None and not preproc_dir.is_dir(): + log.error("The 'preproc_dir' does not exist:\n\t" f"{preproc_dir}") + exit(EXIT_CODES["NOINPUT"]["Value"]) + + if action in {"stats", "contrasts", "results"}: + if preproc_dir is None or not preproc_dir.exists(): + log.error( + "'preproc_dir' must be specified for stats.\n" f"Got: {preproc_dir}" + ) + raise SystemExit(EXIT_CODES["USAGE"]["Value"]) + + if model_file is None or not model_file.exists(): + log.error("'model_file' must be specified for stats.\n" f"Got: {model_file}") + raise SystemExit(EXIT_CODES["USAGE"]["Value"]) + + if action in {"stats", "contrasts", "results", "default_model"} and ( + space and len(space) > 1 + ): + log.error("Only one space allowed for statistical analysis.\n" f"Got: {space}") + raise SystemExit(EXIT_CODES["USAGE"]["Value"]) + + if action == "preprocess" and task and len(task) > 1: + log.error("Only one task allowed for preprocessing.\n" f"Got: {task}") + raise SystemExit(EXIT_CODES["USAGE"]["Value"]) + + return_code = bidspm( + bids_dir, + output_dir, + analysis_level, + action=action, + participant_label=args.participant_label, + verbosity=args.verbosity, + task=task, + space=space, + ignore=args.ignore, + fwhm=args.fwhm, + bids_filter_file=bids_filter_file, + dummy_scans=args.dummy_scans, + anat_only=args.anat_only, + skip_validation=args.skip_validation, + dry_run=args.dry_run, + preproc_dir=preproc_dir, + model_file=model_file, + roi_based=args.roi_based, + roi_atlas=roi_atlas, + roi_name=args.roi_name, + roi_dir=args.roi_dir, + concatenate=args.concatenate, + design_only=args.design_only, + keep_residuals=args.keep_residuals, + options=options, + ) + + if return_code == 1: + raise SystemExit(EXIT_CODES["FAILURE"]["Value"]) + else: + sys.exit(EXIT_CODES["SUCCESS"]["Value"]) + + +def new_cli(argv: Any = sys.argv) -> None: + + parser = sub_command_parser() + + args, _ = parser.parse_known_args(argv[1:]) diff --git a/src/bidspm/data/allowed_actions.json b/src/bidspm/data/allowed_actions.json new file mode 100644 index 000000000..dd47b271f --- /dev/null +++ b/src/bidspm/data/allowed_actions.json @@ -0,0 +1,14 @@ +[ + "bms", + "bms-bms", + "bms-posterior", + "copy", + "contrasts", + "create_roi", + "default_model", + "preprocess", + "results", + "smooth", + "specify_only", + "stats" +] diff --git a/src/bidspm/data/exit_codes.json b/src/bidspm/data/exit_codes.json new file mode 100644 index 000000000..9258b78ff --- /dev/null +++ b/src/bidspm/data/exit_codes.json @@ -0,0 +1,50 @@ +{ + "SUCCESS": { + "Value": 0, + "Description": "The program completed successfully." + }, + "FAILURE": { + "Value": 1, + "Description": "The program failed for unspecified reasons." + }, + "INVALID": { + "Value": 16, + "Description": "An input dataset failed BIDS validation." + }, + "17": { + "Value": 17, + "Description": "Unknown analysis level." + }, + "18": { + "Value": 18, + "Description": "Entity-based filtering options selected no files." + }, + "19": { + "Value": 19, + "Description": "Both command-line arguments and a parameter invocation file were passed to the application." + }, + "USAGE": { + "Value": 64, + "Description": "The command was used incorrectly." + }, + "DATAERR": { + "Value": 65, + "Description": "The input data was incorrect in some way." + }, + "NOINPUT": { + "Value": 66, + "Description": "The input data was missing or unreadable." + }, + "CANTCREAT": { + "Value": 73, + "Description": "An output file/directory cannot be created." + }, + "IOERR": { + "Value": 64, + "Description": "Failure during file reading/writing." + }, + "TEMPFAIL": { + "Value": 75, + "Description": "Temporary failure. Another run is expected to succeed." + } +} diff --git a/src/bidspm/exit_codes.json b/src/bidspm/exit_codes.json deleted file mode 100644 index 0b2e05298..000000000 --- a/src/bidspm/exit_codes.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "0": { - "Value": 0, - "Description": "SUCCESS. The program completed successfully." - }, - "1": { - "Value": 1, - "Description": "FAILURE. The program failed for unspecified reasons." - }, - "16": { - "Value": 16, - "Description": "An input dataset failed BIDS validation." - }, - "17": { - "Value": 17, - "Description": "Unknown analysis level." - }, - "18": { - "Value": 18, - "Description": "Entity-based filtering options selected no files." - }, - "19": { - "Value": 19, - "Description": "Both command-line arguments and a parameter invocation file were passed to the application." - }, - "64": { - "Value": 64, - "Description": "USAGE. The command was used incorrectly." - }, - "65": { - "Value": 65, - "Description": "DATAERR. The input data was incorrect in some way." - }, - "66": { - "Value": 66, - "Description": "NOINPUT. The input data was missing or unreadable." - }, - "73": { - "Value": 73, - "Description": "CANTCREAT. An output file/directory cannot be created." - }, - "74": { - "Value": 64, - "Description": "IOERR. Failure during file reading/writing." - }, - "75": { - "Value": 75, - "Description": "TEMPFAIL. Temporary failure. Another run is expected to succeed." - } -} diff --git a/src/bidspm/parsers.py b/src/bidspm/parsers.py index a1c04759b..c4dcd2a52 100644 --- a/src/bidspm/parsers.py +++ b/src/bidspm/parsers.py @@ -1,7 +1,9 @@ from __future__ import annotations import argparse +import json import logging +from pathlib import Path from rich.logging import RichHandler from rich_argparse import RichHelpFormatter @@ -10,6 +12,18 @@ log = logging.getLogger("bidspm") +with open(Path(__file__).parent / "data" / "allowed_actions.json") as f: + ALLOWED_ACTIONS = json.load(f) + +SUPPORTED_ATLASES = { + "anatomy_toobox", + "glasser", + "hcpex", + "neuromorphometrics", + "visfatlas", + "wang", +} + def bidspm_log(name: str = "bidspm") -> logging.Logger: """Create log.""" @@ -25,7 +39,7 @@ def bidspm_log(name: str = "bidspm") -> logging.Logger: return logging.getLogger(name) -def common_parser() -> argparse.ArgumentParser: +def base_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( description="bidspm is a SPM base BIDS app", epilog=""" @@ -71,34 +85,82 @@ def common_parser() -> argparse.ArgumentParser: type=str, nargs=1, ) + + return parser + + +def add_common_arguments(parser: argparse.ArgumentParser) -> argparse.ArgumentParser: parser.add_argument( - "--action", + "--verbosity", help=""" - Action to perform. + Verbosity level. + """, + choices=[0, 1, 2, 3], + default=2, + type=int, + nargs=1, + ) + + parser.add_argument( + "--bids_filter_file", + help=""" + Fullpath to a JSON file describing custom BIDS input filters. """, - choices=[ - "preprocess", - "smooth", - "default_model", - "create_roi", - "stats", - "contrasts", - "results", - ], - required=True, type=str, nargs=1, ) + parser.add_argument( - "--verbosity", + "--options", help=""" - Verbosity level. + Path to JSON file containing bidspm options. """, - choices=[0, 1, 2, 3], - default=2, - type=int, + ) + + return parser + + +def sub_command_parser() -> argparse.ArgumentParser: + parser = base_parser() + subparsers = parser.add_subparsers( + dest="command", + help="Choose a subcommand", + required=True, + ) + + default_parser = subparsers.add_parser( + "default_model", + help="""Create default model""", + formatter_class=parser.formatter_class, + ) + default_parser = add_common_arguments(default_parser) + + roi_parser = subparsers.add_parser( + "create_roi", + help="""Create ROIs""", + formatter_class=parser.formatter_class, + ) + roi_parser = add_common_arguments(roi_parser) + + return parser + + +def common_parser() -> argparse.ArgumentParser: + parser = base_parser() + + parser.add_argument( + "--action", + help=""" + Action to perform. + """, + choices=ALLOWED_ACTIONS, + required=True, + type=str, nargs=1, ) + + parser = add_common_arguments(parser) + parser.add_argument( "--task", help=""" @@ -163,14 +225,7 @@ def common_parser() -> argparse.ArgumentParser: action="store_true", default=False, ) - parser.add_argument( - "--bids_filter_file", - help=""" - Fullpath to a JSON file describing custom BIDS input filters. - """, - type=str, - nargs=1, - ) + parser.add_argument( "--fwhm", help=""" @@ -181,12 +236,6 @@ def common_parser() -> argparse.ArgumentParser: nargs=1, default=6.0, ) - parser.add_argument( - "--options", - help=""" - Path to JSON file containing bidspm options. - """, - ) parser.add_argument( "--skip_validation", help=""" @@ -231,7 +280,7 @@ def common_parser() -> argparse.ArgumentParser: default=0, ) - create_roi_only = parser.add_argument_group("create roi only arguments") + create_roi_only = parser.add_argument_group("create_roi and stats only arguments") create_roi_only.add_argument( "--roi_atlas", help=""" @@ -240,7 +289,7 @@ def common_parser() -> argparse.ArgumentParser: type=str, nargs=1, default="neuromorphometrics", - choices=["neuromorphometrics", "wang", "anatomy_toobox", "visfatlas", "hcpex"], + choices=SUPPORTED_ATLASES, ) stats_only = parser.add_argument_group("stats only arguments") diff --git a/src/cli/cliDefaultModel.m b/src/cli/cliDefaultModel.m index 18525886a..7c76864e6 100644 --- a/src/cli/cliDefaultModel.m +++ b/src/cli/cliDefaultModel.m @@ -5,6 +5,7 @@ function cliDefaultModel(varargin) % % (C) Copyright 2023 bidspm developers + args = inputParserForCreateModel(); try parse(args, varargin{:}); diff --git a/src/cli/getOptionsFromCliArgument.m b/src/cli/getOptionsFromCliArgument.m index c776c7675..27b55f387 100644 --- a/src/cli/getOptionsFromCliArgument.m +++ b/src/cli/getOptionsFromCliArgument.m @@ -133,23 +133,6 @@ end end -function value = bidsAppsActions() - - value = {'copy'; ... - 'create_roi'; ... - 'preprocess'; ... - 'smooth'; ... - 'default_model'; ... - 'stats'; ... - 'contrasts'; ... - 'results'; ... - 'specify_only'; ... - 'bms'; ... - 'bms-posterior'; ... - 'bms-bms' ... - }; -end - function opt = getOptions(args) if isstruct(args.Results.options) diff --git a/src/constants/bidsAppsActions.m b/src/constants/bidsAppsActions.m new file mode 100644 index 000000000..14d13b0cc --- /dev/null +++ b/src/constants/bidsAppsActions.m @@ -0,0 +1,29 @@ +function value = bidsAppsActions() + % Returns a cell array of the supported bids apps actions + + % (C) Copyright 2022 bidspm developers + + try + input_file = fullfile(fileparts(mfilename('fullpath')), ... + '..', ... + 'bidspm', ... + 'data', ... + 'allowed_actions.json'); + + value = bids.util.jsondecode(input_file); + catch + value = {'copy'; ... + 'create_roi'; ... + 'preprocess'; ... + 'smooth'; ... + 'default_model'; ... + 'stats'; ... + 'contrasts'; ... + 'results'; ... + 'specify_only'; ... + 'bms'; ... + 'bms-posterior'; ... + 'bms-bms'}; + end + +end diff --git a/tests/test_bidspm.py b/tests/test_bidspm.py index f7602bd74..67b77f9e0 100644 --- a/tests/test_bidspm.py +++ b/tests/test_bidspm.py @@ -9,14 +9,13 @@ append_base_arguments, append_common_arguments, base_cmd, - bidspm, create_roi, default_model, + new_line, preprocess, run_command, stats, ) -from bidspm.parsers import common_parser def test_base_cmd(): @@ -24,28 +23,7 @@ def test_base_cmd(): bids_dir = Path("/path/to/bids") output_dir = Path("/path/to/output") cmd = base_cmd(bids_dir, output_dir) - assert cmd == " bidspm('init'); bidspm( '/path/to/bids', ...\n\t '/path/to/output'" - - -def test_parser(): - """Test parser.""" - parser = common_parser() - assert parser.description == "bidspm is a SPM base BIDS app" - - args, _ = parser.parse_known_args( - [ - "/path/to/bids", - "/path/to/output", - "subject", - "--action", - "preprocess", - "--task", - "foo", - "bar", - ] - ) - - assert args.task == ["foo", "bar"] + assert cmd == f" bidspm( '/path/to/bids'{new_line}'/path/to/output'" def test_append_common_arguments(): @@ -56,9 +34,9 @@ def test_append_common_arguments(): skip_validation=True, dry_run=True, ) - assert ( - cmd - == ", ...\n\t 'fwhm', 6, ...\n\t 'participant_label', { '01', '02' }, ...\n\t 'skip_validation', true, ...\n\t 'dry_run', true" + assert cmd == ( + f"{new_line}'fwhm', 6{new_line}'participant_label', {'{'} '01', '02' {'}'}" + f"{new_line}'skip_validation', true{new_line}'dry_run', true" ) @@ -66,9 +44,9 @@ def test_append_base_arguments(): cmd = append_base_arguments( cmd="", verbosity=0, space=["foo", "bar"], task=["spam", "eggs"], ignore=["nii"] ) - assert ( - cmd - == ", ...\n\t 'verbosity', 0, ...\n\t 'space', { 'foo', 'bar' }, ...\n\t 'task', { 'spam', 'eggs' }, ...\n\t 'ignore', { 'nii' }" + assert cmd == ( + f"{new_line}'verbosity', 0{new_line}'space', {'{'} 'foo', 'bar' {'}'}" + f"{new_line}'task', {'{'} 'spam', 'eggs' {'}'}{new_line}'ignore', {'{'} 'nii' {'}'}" ) @@ -79,30 +57,6 @@ def test_run_command(): assert return_code == 0 -def test_bidspm_error_dir(caplog): - return_code = bidspm( - bids_dir=Path("/foo/bar"), - output_dir=Path, - analysis_level="subject", - action="preprocess", - ) - assert return_code == 1 - assert ["The 'bids_dir' does not exist:\n\t/foo/bar"] == [ - rec.message for rec in caplog.records - ] - - -def test_bidspm_error_action(caplog): - return_code = bidspm( - bids_dir=Path(), - output_dir=Path, - analysis_level="subject", - action="spam", - ) - assert return_code == 1 - assert ["\nunknown action: spam"] == [rec.message for rec in caplog.records] - - @pytest.mark.parametrize( "action", [ diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 000000000..b436163f3 --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,26 @@ +from pathlib import Path + +import pytest + +from bidspm.cli import cli + + +def test_bidspm_error_dir(caplog): + + with pytest.raises(SystemExit) as pytest_wrapped_e: + cli(["bidspm", "/foo/bar", str(Path()), "subject", "--action", "preprocess"]) + + assert pytest_wrapped_e.type == SystemExit + assert pytest_wrapped_e.value.code == 66 + assert ["The 'bids_dir' does not exist:\n\t/foo/bar"] == [ + rec.message for rec in caplog.records + ] + + +def test_bidspm_error_action(caplog): + with pytest.raises(SystemExit) as pytest_wrapped_e: + cli(["bidspm", str(Path()), str(Path()), "subject", "--action", "bms"]) + + assert pytest_wrapped_e.type == SystemExit + assert pytest_wrapped_e.value.code == 64 + assert any("not yet implemented" in rec.message for rec in caplog.records) diff --git a/tests/test_parsers.py b/tests/test_parsers.py new file mode 100644 index 000000000..4104d8261 --- /dev/null +++ b/tests/test_parsers.py @@ -0,0 +1,22 @@ +from bidspm.parsers import common_parser + + +def test_parser(): + """Test parser.""" + parser = common_parser() + assert parser.description == "bidspm is a SPM base BIDS app" + + args, _ = parser.parse_known_args( + [ + "/path/to/bids", + "/path/to/output", + "subject", + "--action", + "preprocess", + "--task", + "foo", + "bar", + ] + ) + + assert args.task == ["foo", "bar"] diff --git a/tests/tests_unit/test_setDirectories.m b/tests/tests_unit/test_setDirectories.m index 6a59ebbae..48f1dd972 100644 --- a/tests/tests_unit/test_setDirectories.m +++ b/tests/tests_unit/test_setDirectories.m @@ -66,7 +66,7 @@ function test_setDirectories_inputs_outputs() % expected = defaultOptions(); - baseDir = fullfile(fileparts(mfilename('fullpath'))); + baseDir = fileparts(mfilename('fullpath')); expected.dir.raw = fullfile(baseDir, 'inputs', 'raw'); expected.dir.derivatives = fullfile(baseDir, 'outputs', 'derivatives'); expected.dir.preproc = fullfile(expected.dir.derivatives, 'bidspm-preproc');