From 468184b1eede7a7de7c04362df205ba0849d3ff2 Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Thu, 26 Mar 2026 10:52:34 -0400 Subject: [PATCH 1/4] Convert CLIs to subcommands in modelarrayio CLI. --- docs/examples/plot_fixel_workflow.py | 8 +- docs/examples/plot_voxel_workflow.py | 8 +- docs/usage.rst | 48 +++---- pyproject.toml | 7 +- src/modelarrayio/cli/cifti_to_h5.py | 62 +++++++-- src/modelarrayio/cli/h5_to_cifti.py | 29 ++-- src/modelarrayio/cli/h5_to_mif.py | 30 ++--- src/modelarrayio/cli/h5_to_nifti.py | 36 ++--- src/modelarrayio/cli/main.py | 53 ++++++++ src/modelarrayio/cli/mif_to_h5.py | 193 +++++++++------------------ src/modelarrayio/cli/nifti_to_h5.py | 60 +++++++-- test/test_cifti_cli.py | 35 ++--- test/test_voxels_cli.py | 38 ++---- 13 files changed, 315 insertions(+), 292 deletions(-) create mode 100644 src/modelarrayio/cli/main.py diff --git a/docs/examples/plot_fixel_workflow.py b/docs/examples/plot_fixel_workflow.py index cd94d88..9f72a65 100644 --- a/docs/examples/plot_fixel_workflow.py +++ b/docs/examples/plot_fixel_workflow.py @@ -98,7 +98,7 @@ # # activate your conda environment first # conda activate # -# confixel \ +# modelarrayio mif-to-h5 \ # --index-file /home/username/myProject/data/FD/index.mif \ # --directions-file /home/username/myProject/data/FD/directions.mif \ # --cohort-file /home/username/myProject/data/cohort_FD.csv \ @@ -118,7 +118,7 @@ # # .. code-block:: console # -# fixelstats_write \ +# modelarrayio h5-to-mif \ # --index-file /home/username/myProject/data/FD/index.mif \ # --directions-file /home/username/myProject/data/FD/directions.mif \ # --cohort-file /home/username/myProject/data/cohort_FD.csv \ @@ -144,7 +144,7 @@ # # .. code-block:: console # -# confixel --help -# fixelstats_write --help +# modelarrayio mif-to-h5 --help +# modelarrayio h5-to-mif --help # # or in the :doc:`/usage` page of this documentation. diff --git a/docs/examples/plot_voxel_workflow.py b/docs/examples/plot_voxel_workflow.py index 87a24f0..c0a810e 100644 --- a/docs/examples/plot_voxel_workflow.py +++ b/docs/examples/plot_voxel_workflow.py @@ -104,7 +104,7 @@ # # activate your conda environment first # conda activate # -# convoxel \ +# modelarrayio nifti-to-h5 \ # --group-mask-file /home/username/myProject/data/group_mask.nii.gz \ # --cohort-file /home/username/myProject/data/cohort_FA.csv \ # --output-hdf5 /home/username/myProject/data/FA.h5 @@ -121,7 +121,7 @@ # # .. code-block:: console # -# volumestats_write \ +# modelarrayio h5-to-nifti \ # --group-mask-file /home/username/myProject/data/group_mask.nii.gz \ # --cohort-file /home/username/myProject/data/cohort_FA.csv \ # --analysis-name mylm \ @@ -177,7 +177,7 @@ # # .. code-block:: console # -# convoxel --help -# volumestats_write --help +# modelarrayio nifti-to-h5 --help +# modelarrayio h5-to-nifti --help # # or in the :doc:`/usage` page of this documentation. diff --git a/docs/usage.rst b/docs/usage.rst index a2e27fc..244bdd0 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -3,60 +3,60 @@ Usage ##### -******** -confixel -******** +********** +mif-to-h5 +********** .. argparse:: :ref: modelarrayio.cli.mif_to_h5.get_parser - :prog: confixel + :prog: modelarrayio mif-to-h5 :func: get_parser -******** -convoxel -******** +********** +nifti-to-h5 +********** .. argparse:: :ref: modelarrayio.cli.nifti_to_h5.get_parser - :prog: convoxel + :prog: modelarrayio nifti-to-h5 :func: get_parser -******** -concifti -******** +********** +cifti-to-h5 +********** .. argparse:: :ref: modelarrayio.cli.cifti_to_h5.get_parser - :prog: concifti + :prog: modelarrayio cifti-to-h5 :func: get_parser -**************** -fixelstats_write -**************** +********** +h5-to-mif +********** .. argparse:: :ref: modelarrayio.cli.h5_to_mif.get_parser - :prog: fixelstats_write + :prog: modelarrayio h5-to-mif :func: get_parser -***************** -volumestats_write -***************** +*********** +h5-to-nifti +*********** .. argparse:: :ref: modelarrayio.cli.h5_to_nifti.get_parser - :prog: volumestats_write + :prog: modelarrayio h5-to-nifti :func: get_parser -**************** -ciftistats_write -**************** +*********** +h5-to-cifti +*********** .. argparse:: :ref: modelarrayio.cli.h5_to_cifti.get_parser - :prog: ciftistats_write + :prog: modelarrayio h5-to-cifti :func: get_parser diff --git a/pyproject.toml b/pyproject.toml index 5727360..50d42d8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,12 +67,7 @@ Repository = "https://github.com/PennLINC/ModelArrayIO" Documentation = "https://modelarrayio.readthedocs.io" [project.scripts] -confixel = "modelarrayio.cli.mif_to_h5:main" -convoxel = "modelarrayio.cli.nifti_to_h5:main" -concifti = "modelarrayio.cli.cifti_to_h5:main" -fixelstats_write = "modelarrayio.cli.h5_to_mif:main" -volumestats_write = "modelarrayio.cli.h5_to_nifti:main" -ciftistats_write = "modelarrayio.cli.h5_to_cifti:main" +modelarrayio = "modelarrayio.cli.main:main" # # Hatch diff --git a/src/modelarrayio/cli/cifti_to_h5.py b/src/modelarrayio/cli/cifti_to_h5.py index ff3fc86..3dabd5c 100644 --- a/src/modelarrayio/cli/cifti_to_h5.py +++ b/src/modelarrayio/cli/cifti_to_h5.py @@ -204,7 +204,55 @@ def _process_scalar_job(scalar_name, source_files): return 0 -def get_parser(): +def cifti_to_h5_main( + cohort_file, + backend='hdf5', + output_hdf5='fixelarray.h5', + output_tiledb='arraydb.tdb', + storage_dtype='float32', + compression='gzip', + compression_level=4, + shuffle=True, + chunk_voxels=0, + target_chunk_mb=2.0, + tdb_compression='zstd', + tdb_compression_level=5, + tdb_shuffle=True, + tdb_tile_voxels=0, + tdb_target_tile_mb=2.0, + tdb_workers=None, + scalar_columns=None, + s3_workers=1, + log_level='INFO', +): + """Entry point for the ``modelarrayio cifti-to-h5`` command.""" + logging.basicConfig( + level=getattr(logging, str(log_level).upper(), logging.INFO), + format='[%(levelname)s] %(name)s: %(message)s', + ) + return cifti_to_h5( + cohort_file=cohort_file, + backend=backend, + output_hdf5=output_hdf5, + output_tiledb=output_tiledb, + storage_dtype=storage_dtype, + compression=compression, + compression_level=compression_level, + shuffle=shuffle, + chunk_voxels=chunk_voxels, + target_chunk_mb=target_chunk_mb, + tdb_compression=tdb_compression, + tdb_compression_level=tdb_compression_level, + tdb_shuffle=tdb_shuffle, + tdb_tile_voxels=tdb_tile_voxels, + tdb_target_tile_mb=tdb_target_tile_mb, + tdb_workers=tdb_workers, + scalar_columns=scalar_columns, + s3_workers=s3_workers, + ) + + +def _parse_cifti_to_h5(): parser = argparse.ArgumentParser( description='Create a hdf5 file of CIDTI2 dscalar data', formatter_class=argparse.ArgumentDefaultsHelpFormatter, @@ -228,15 +276,3 @@ def get_parser(): ) add_s3_workers_arg(parser) return parser - - -def main(): - parser = get_parser() - args = parser.parse_args() - kwargs = vars(args) - log_level = kwargs.pop('log_level') - logging.basicConfig( - level=getattr(logging, str(log_level).upper(), logging.INFO), - format='[%(levelname)s] %(name)s: %(message)s', - ) - return cifti_to_h5(**kwargs) diff --git a/src/modelarrayio/cli/h5_to_cifti.py b/src/modelarrayio/cli/h5_to_cifti.py index f5eb7d5..10d3ad6 100644 --- a/src/modelarrayio/cli/h5_to_cifti.py +++ b/src/modelarrayio/cli/h5_to_cifti.py @@ -90,33 +90,34 @@ def h5_to_cifti(example_cifti, in_file, analysis_name, output_dir): temp_nifti2_1mpvalue.to_filename(out_cifti_1mpvalue) -def main(): - """Write the contents of an hdf5 file to a cifti directory.""" - parser = get_parser() - args = parser.parse_args() - - if os.path.exists(args.output_dir): +def h5_to_cifti_main( + analysis_name, + in_file, + output_dir, + cohort_file=None, + example_cifti=None, +): + """Entry point for the ``modelarrayio h5-to-cifti`` command.""" + if os.path.exists(output_dir): print('WARNING: Output directory exists') - os.makedirs(args.output_dir, exist_ok=True) + os.makedirs(output_dir, exist_ok=True) - # Get an example cifti - example_cifti = args.example_cifti if example_cifti is None: logger.warning( 'No example cifti file provided, using the first cifti file from the cohort file' ) - cohort_df = pd.read_csv(args.cohort_file) + cohort_df = pd.read_csv(cohort_file) example_cifti = cohort_df['source_file'][0] h5_to_cifti( example_cifti=example_cifti, - in_file=args.in_file, - analysis_name=args.analysis_name, - output_dir=args.output_dir, + in_file=in_file, + analysis_name=analysis_name, + output_dir=output_dir, ) -def get_parser(): +def _parse_h5_to_cifti(): parser = argparse.ArgumentParser( description='Create a directory with cifti results from an hdf5 file', formatter_class=argparse.ArgumentDefaultsHelpFormatter, diff --git a/src/modelarrayio/cli/h5_to_mif.py b/src/modelarrayio/cli/h5_to_mif.py index f5a4b21..750a45a 100644 --- a/src/modelarrayio/cli/h5_to_mif.py +++ b/src/modelarrayio/cli/h5_to_mif.py @@ -92,36 +92,32 @@ def h5_to_mif(example_mif, in_file, analysis_name, output_dir): nifti2_to_mif(temp_nifti2_1mpvalue, out_mif_1mpvalue) -def main(): - parser = get_parser() - args = parser.parse_args() - - if os.path.exists(args.output_dir): +def h5_to_mif_main(index_file, directions_file, cohort_file, analysis_name, in_file, output_dir): + """Entry point for the ``modelarrayio h5-to-mif`` command.""" + if os.path.exists(output_dir): print('WARNING: Output directory exists') - os.makedirs(args.output_dir, exist_ok=True) + os.makedirs(output_dir, exist_ok=True) - # Copy in the index and directions shutil.copyfile( - args.directions_file, - os.path.join(args.output_dir, os.path.basename(args.directions_file)), + directions_file, + os.path.join(output_dir, os.path.basename(directions_file)), ) shutil.copyfile( - args.index_file, - os.path.join(args.output_dir, os.path.basename(args.index_file)), + index_file, + os.path.join(output_dir, os.path.basename(index_file)), ) - # Get an example mif file - cohort_df = pd.read_csv(args.cohort_file) + cohort_df = pd.read_csv(cohort_file) example_mif = cohort_df['source_file'][0] h5_to_mif( example_mif=example_mif, - in_file=args.in_file, - analysis_name=args.analysis_name, - output_dir=args.output_dir, + in_file=in_file, + analysis_name=analysis_name, + output_dir=output_dir, ) -def get_parser(): +def _parse_h5_to_mif(): parser = argparse.ArgumentParser( description='Create a fixel directory from an hdf5 file', formatter_class=argparse.ArgumentDefaultsHelpFormatter, diff --git a/src/modelarrayio/cli/h5_to_nifti.py b/src/modelarrayio/cli/h5_to_nifti.py index 0614471..30cd5d2 100644 --- a/src/modelarrayio/cli/h5_to_nifti.py +++ b/src/modelarrayio/cli/h5_to_nifti.py @@ -126,18 +126,28 @@ def _decode_names(arr): output_img_1mpvalue.to_filename(out_file_1mpvalue) -def main(): - parser = get_parser() - args = parser.parse_args() - - if os.path.exists(args.output_dir): +def h5_to_nifti_main( + group_mask_file, + analysis_name, + in_file, + output_dir, + output_extension='.nii.gz', +): + """Entry point for the ``modelarrayio h5-to-nifti`` command.""" + if os.path.exists(output_dir): print('WARNING: Output directory exists') - os.makedirs(args.output_dir, exist_ok=True) - - h5_to_nifti(*vars(args)) + os.makedirs(output_dir, exist_ok=True) + + h5_to_nifti( + in_file=in_file, + analysis_name=analysis_name, + group_mask_file=group_mask_file, + output_extension=output_extension, + output_dir=output_dir, + ) -def get_parser(): +def _parse_h5_to_nifti(): parser = argparse.ArgumentParser( description='Convert statistical results from an hdf5 file to a volume data (NIfTI file)', formatter_class=argparse.ArgumentDefaultsHelpFormatter, @@ -150,13 +160,6 @@ def get_parser(): required=True, type=IsFile, ) - parser.add_argument( - '--cohort-file', - '--cohort_file', - help='Path to a csv with demographic info and paths to data.', - required=True, - type=IsFile, - ) parser.add_argument( '--analysis-name', '--analysis_name', @@ -180,6 +183,7 @@ def get_parser(): parser.add_argument( '--output-ext', '--output_ext', + dest='output_extension', help=( 'The extension for output volume data. ' 'Options are .nii.gz (default) and .nii. Please provide the prefix dot.' diff --git a/src/modelarrayio/cli/main.py b/src/modelarrayio/cli/main.py new file mode 100644 index 0000000..4be0fbc --- /dev/null +++ b/src/modelarrayio/cli/main.py @@ -0,0 +1,53 @@ +"""ModelArrayIO command-line interface.""" + +import argparse + +from modelarrayio.cli.cifti_to_h5 import _parse_cifti_to_h5, cifti_to_h5_main +from modelarrayio.cli.h5_to_cifti import _parse_h5_to_cifti, h5_to_cifti_main +from modelarrayio.cli.h5_to_mif import _parse_h5_to_mif, h5_to_mif_main +from modelarrayio.cli.h5_to_nifti import _parse_h5_to_nifti, h5_to_nifti_main +from modelarrayio.cli.mif_to_h5 import _parse_mif_to_h5, mif_to_h5_main +from modelarrayio.cli.nifti_to_h5 import _parse_nifti_to_h5, nifti_to_h5_main + +COMMANDS = [ + ('mif-to-h5', _parse_mif_to_h5, mif_to_h5_main), + ('nifti-to-h5', _parse_nifti_to_h5, nifti_to_h5_main), + ('cifti-to-h5', _parse_cifti_to_h5, cifti_to_h5_main), + ('h5-to-mif', _parse_h5_to_mif, h5_to_mif_main), + ('h5-to-nifti', _parse_h5_to_nifti, h5_to_nifti_main), + ('h5-to-cifti', _parse_h5_to_cifti, h5_to_cifti_main), +] + + +def _get_parser(): + from modelarrayio.__about__ import __version__ + + parser = argparse.ArgumentParser(prog='modelarrayio', allow_abbrev=False) + parser.add_argument( + '-V', '--version', action='version', version=f'modelarrayio {__version__}' + ) + subparsers = parser.add_subparsers(help='modelarrayio subcommands') + + for command, parser_func, run_func in COMMANDS: + subparser = parser_func() + subparser.set_defaults(func=run_func) + subparsers.add_parser( + command, + parents=[subparser], + help=subparser.description, + add_help=False, + allow_abbrev=False, + ) + + return parser + + +def main(argv=None): + """Entry point for the ``modelarrayio`` command.""" + options = _get_parser().parse_args(argv) + if not hasattr(options, 'func'): + _get_parser().print_help() + return 1 + args = vars(options).copy() + args.pop('func') + return options.func(**args) diff --git a/src/modelarrayio/cli/mif_to_h5.py b/src/modelarrayio/cli/mif_to_h5.py index d05bb92..d2f95e8 100644 --- a/src/modelarrayio/cli/mif_to_h5.py +++ b/src/modelarrayio/cli/mif_to_h5.py @@ -10,7 +10,15 @@ import pandas as pd from tqdm import tqdm -from modelarrayio.cli.parser_utils import _is_file +from modelarrayio.cli.parser_utils import ( + _is_file, + add_backend_arg, + add_cohort_arg, + add_output_hdf5_arg, + add_output_tiledb_arg, + add_storage_args, + add_tiledb_storage_args, +) from modelarrayio.storage import h5_storage, tiledb_storage from modelarrayio.utils.fixels import gather_fixels, mif_to_nifti2 @@ -154,7 +162,53 @@ def mif_to_h5( return 0 -def get_parser(): +def mif_to_h5_main( + index_file, + directions_file, + cohort_file, + backend='hdf5', + output_hdf5='fixelarray.h5', + output_tiledb='arraydb.tdb', + storage_dtype='float32', + compression='gzip', + compression_level=4, + shuffle=True, + chunk_voxels=0, + target_chunk_mb=2.0, + tdb_compression='zstd', + tdb_compression_level=5, + tdb_shuffle=True, + tdb_tile_voxels=0, + tdb_target_tile_mb=2.0, + log_level='INFO', +): + """Entry point for the ``modelarrayio mif-to-h5`` command.""" + logging.basicConfig( + level=getattr(logging, str(log_level).upper(), logging.INFO), + format='[%(levelname)s] %(name)s: %(message)s', + ) + return mif_to_h5( + index_file=index_file, + directions_file=directions_file, + cohort_file=cohort_file, + backend=backend, + output_hdf5=Path(output_hdf5), + output_tiledb=Path(output_tiledb), + storage_dtype=storage_dtype, + compression=compression, + compression_level=compression_level, + shuffle=shuffle, + chunk_voxels=chunk_voxels, + target_chunk_mb=target_chunk_mb, + tdb_compression=tdb_compression, + tdb_compression_level=tdb_compression_level, + tdb_shuffle=tdb_shuffle, + tdb_tile_voxels=tdb_tile_voxels, + tdb_target_tile_mb=tdb_target_tile_mb, + ) + + +def _parse_mif_to_h5(): parser = argparse.ArgumentParser( description='Create a hdf5 file of fixel data', formatter_class=argparse.ArgumentDefaultsHelpFormatter, @@ -175,133 +229,10 @@ def get_parser(): required=True, type=IsFile, ) - parser.add_argument( - '--cohort-file', - '--cohort_file', - help='Path to a csv with demographic info and paths to data.', - required=True, - type=IsFile, - ) - parser.add_argument( - '--output-hdf5', - '--output_hdf5', - help='Name of HDF5 (.h5) file where outputs will be saved.', - default='fixelarray.h5', - ) - parser.add_argument( - '--output-tiledb', - '--output_tiledb', - help='Base URI (directory) where TileDB arrays will be created.', - default='arraydb.tdb', - ) - parser.add_argument( - '--backend', - help='Storage backend for subject-by-element matrix', - choices=['hdf5', 'tiledb'], - default='hdf5', - ) - # Storage configuration (match voxels.py) - parser.add_argument( - '--dtype', - help='Floating dtype for storing values: float32 (default) or float64', - choices=['float32', 'float64'], - default='float32', - dest='storage_dtype', - ) - parser.add_argument( - '--compression', - help='HDF5 compression filter: gzip (default), lzf, none', - choices=['gzip', 'lzf', 'none'], - default='gzip', - ) - parser.add_argument( - '--tdb-compression', - '--tdb_compression', - help='TileDB compression: zstd (default), gzip, none', - choices=['zstd', 'gzip', 'none'], - default='zstd', - ) - parser.add_argument( - '--compression-level', - '--compression_level', - type=int, - help='Gzip compression level 0-9 (only if --compression=gzip). Default 4', - default=4, - ) - parser.add_argument( - '--tdb-compression-level', - '--tdb_compression_level', - type=int, - help='Compression level for TileDB (codec-dependent).', - default=5, - ) - parser.add_argument( - '--no-shuffle', - dest='shuffle', - action='store_false', - help='Disable HDF5 shuffle filter (enabled by default if compression is used).', - default=True, - ) - parser.add_argument( - '--tdb-no-shuffle', - dest='tdb_shuffle', - action='store_false', - help='Disable TileDB shuffle filter (enabled by default).', - default=True, - ) - parser.add_argument( - '--chunk-voxels', - '--chunk_voxels', - type=int, - help=( - 'Chunk size along fixel/voxel axis. ' - 'If 0, auto-compute based on --target-chunk-mb and number of subjects' - ), - default=0, - ) - parser.add_argument( - '--tdb-tile-voxels', - '--tdb_tile_voxels', - type=int, - help=( - 'Tile length along item axis for TileDB. ' - 'If 0, auto-compute based on --tdb-target-tile-mb' - ), - default=0, - ) - parser.add_argument( - '--target-chunk-mb', - '--target_chunk_mb', - type=float, - help='Target chunk size in MiB when auto-computing item chunk length. Default 2.0', - default=2.0, - ) - parser.add_argument( - '--tdb-target-tile-mb', - '--tdb_target_tile_mb', - type=float, - help='Target tile size in MiB when auto-computing item tile length. Default 2.0', - default=2.0, - ) - parser.add_argument( - '--log-level', - '--log_level', - type=str, - choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'], - help='Logging level (default INFO; set to WARNING to reduce verbosity)', - default='INFO', - ) + add_cohort_arg(parser) + add_output_hdf5_arg(parser, default_name='fixelarray.h5') + add_output_tiledb_arg(parser, default_name='arraydb.tdb') + add_backend_arg(parser) + add_storage_args(parser) + add_tiledb_storage_args(parser) return parser - - -def main(): - """Main function to write fixel data to an HDF5 or TileDB file.""" - parser = get_parser() - args = parser.parse_args() - kwargs = vars(args) - log_level = kwargs.pop('log_level') - logging.basicConfig( - level=getattr(logging, str(log_level).upper(), logging.INFO), - format='[%(levelname)s] %(name)s: %(message)s', - ) - return mif_to_h5(**kwargs) diff --git a/src/modelarrayio/cli/nifti_to_h5.py b/src/modelarrayio/cli/nifti_to_h5.py index 29b6ed1..c290290 100644 --- a/src/modelarrayio/cli/nifti_to_h5.py +++ b/src/modelarrayio/cli/nifti_to_h5.py @@ -157,7 +157,53 @@ def nifti_to_h5( return 0 -def get_parser(): +def nifti_to_h5_main( + group_mask_file, + cohort_file, + backend='hdf5', + output_hdf5='voxeldb.h5', + output_tiledb='arraydb.tdb', + storage_dtype='float32', + compression='gzip', + compression_level=4, + shuffle=True, + chunk_voxels=0, + target_chunk_mb=2.0, + tdb_compression='zstd', + tdb_compression_level=5, + tdb_shuffle=True, + tdb_tile_voxels=0, + tdb_target_tile_mb=2.0, + s3_workers=1, + log_level='INFO', +): + """Entry point for the ``modelarrayio nifti-to-h5`` command.""" + logging.basicConfig( + level=getattr(logging, str(log_level).upper(), logging.INFO), + format='[%(levelname)s] %(name)s: %(message)s', + ) + return nifti_to_h5( + group_mask_file=group_mask_file, + cohort_file=cohort_file, + backend=backend, + output_hdf5=output_hdf5, + output_tiledb=output_tiledb, + storage_dtype=storage_dtype, + compression=compression, + compression_level=compression_level, + shuffle=shuffle, + chunk_voxels=chunk_voxels, + target_chunk_mb=target_chunk_mb, + tdb_compression=tdb_compression, + tdb_compression_level=tdb_compression_level, + tdb_shuffle=tdb_shuffle, + tdb_tile_voxels=tdb_tile_voxels, + tdb_target_tile_mb=tdb_target_tile_mb, + s3_workers=s3_workers, + ) + + +def _parse_nifti_to_h5(): parser = argparse.ArgumentParser( description='Create a hdf5 file of volume data', formatter_class=argparse.ArgumentDefaultsHelpFormatter, @@ -178,15 +224,3 @@ def get_parser(): add_tiledb_storage_args(parser) add_s3_workers_arg(parser) return parser - - -def main(): - parser = get_parser() - args = parser.parse_args() - kwargs = vars(args) - log_level = kwargs.pop('log_level') - logging.basicConfig( - level=getattr(logging, str(log_level).upper(), logging.INFO), - format='[%(levelname)s] %(name)s: %(message)s', - ) - return nifti_to_h5(**kwargs) diff --git a/test/test_cifti_cli.py b/test/test_cifti_cli.py index feb6bd2..94209a7 100644 --- a/test/test_cifti_cli.py +++ b/test/test_cifti_cli.py @@ -1,13 +1,12 @@ import csv import os.path as op -import sys import h5py import nibabel as nb import numpy as np from nibabel.cifti2.cifti2_axes import BrainModelAxis, ScalarAxis -from modelarrayio.cli.cifti_to_h5 import main as concifti_main +from modelarrayio.cli.main import main as modelarrayio_main def _make_synthetic_cifti_dscalar(mask_bool: np.ndarray, values: np.ndarray) -> nb.Cifti2Image: @@ -53,28 +52,16 @@ def test_concifti_cli_creates_expected_hdf5(tmp_path, monkeypatch): out_h5 = tmp_path / 'out_cifti.h5' monkeypatch.chdir(tmp_path) - monkeypatch.setattr( - sys, - 'argv', - [ - 'concifti', - '--cohort-file', - str(cohort_csv), - '--output-hdf5', - str(out_h5), - '--backend', - 'hdf5', - '--dtype', - 'float32', - '--compression', - 'gzip', - '--compression-level', - '1', - '--target-chunk-mb', - '1.0', - ], - ) - assert concifti_main() == 0 + assert modelarrayio_main([ + 'cifti-to-h5', + '--cohort-file', str(cohort_csv), + '--output-hdf5', str(out_h5), + '--backend', 'hdf5', + '--dtype', 'float32', + '--compression', 'gzip', + '--compression-level', '1', + '--target-chunk-mb', '1.0', + ]) == 0 assert op.exists(out_h5) # Validate HDF5 contents diff --git a/test/test_voxels_cli.py b/test/test_voxels_cli.py index 3e4313b..895cccb 100644 --- a/test/test_voxels_cli.py +++ b/test/test_voxels_cli.py @@ -1,12 +1,11 @@ import csv import os.path as op -import sys import h5py import nibabel as nb import numpy as np -from modelarrayio.cli.nifti_to_h5 import main as convoxel_main +from modelarrayio.cli.main import main as modelarrayio_main def _make_nifti(data, affine=None): @@ -72,30 +71,17 @@ def test_convoxel_cli_creates_expected_hdf5(tmp_path, monkeypatch): out_h5 = tmp_path / 'out.h5' monkeypatch.chdir(tmp_path) - monkeypatch.setattr( - sys, - 'argv', - [ - 'convoxel', - '--group-mask-file', - str(group_mask_file), - '--cohort-file', - str(cohort_csv), - '--output-hdf5', - str(out_h5), - '--backend', - 'hdf5', - '--dtype', - 'float32', - '--compression', - 'gzip', - '--compression-level', - '1', - '--target-chunk-mb', - '1.0', - ], - ) - assert convoxel_main() == 0 + assert modelarrayio_main([ + 'nifti-to-h5', + '--group-mask-file', str(group_mask_file), + '--cohort-file', str(cohort_csv), + '--output-hdf5', str(out_h5), + '--backend', 'hdf5', + '--dtype', 'float32', + '--compression', 'gzip', + '--compression-level', '1', + '--target-chunk-mb', '1.0', + ]) == 0 assert op.exists(out_h5) # Validate HDF5 contents From 263c60f96be175350b741f39e581277fa7f81537 Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Thu, 26 Mar 2026 10:57:14 -0400 Subject: [PATCH 2/4] Run ruff. --- src/modelarrayio/cli/main.py | 4 +--- test/test_cifti_cli.py | 32 ++++++++++++++++++++++---------- test/test_voxels_cli.py | 35 ++++++++++++++++++++++++----------- 3 files changed, 47 insertions(+), 24 deletions(-) diff --git a/src/modelarrayio/cli/main.py b/src/modelarrayio/cli/main.py index 4be0fbc..d5fe40c 100644 --- a/src/modelarrayio/cli/main.py +++ b/src/modelarrayio/cli/main.py @@ -23,9 +23,7 @@ def _get_parser(): from modelarrayio.__about__ import __version__ parser = argparse.ArgumentParser(prog='modelarrayio', allow_abbrev=False) - parser.add_argument( - '-V', '--version', action='version', version=f'modelarrayio {__version__}' - ) + parser.add_argument('-V', '--version', action='version', version=f'modelarrayio {__version__}') subparsers = parser.add_subparsers(help='modelarrayio subcommands') for command, parser_func, run_func in COMMANDS: diff --git a/test/test_cifti_cli.py b/test/test_cifti_cli.py index 94209a7..27a6039 100644 --- a/test/test_cifti_cli.py +++ b/test/test_cifti_cli.py @@ -52,16 +52,28 @@ def test_concifti_cli_creates_expected_hdf5(tmp_path, monkeypatch): out_h5 = tmp_path / 'out_cifti.h5' monkeypatch.chdir(tmp_path) - assert modelarrayio_main([ - 'cifti-to-h5', - '--cohort-file', str(cohort_csv), - '--output-hdf5', str(out_h5), - '--backend', 'hdf5', - '--dtype', 'float32', - '--compression', 'gzip', - '--compression-level', '1', - '--target-chunk-mb', '1.0', - ]) == 0 + assert ( + modelarrayio_main( + [ + 'cifti-to-h5', + '--cohort-file', + str(cohort_csv), + '--output-hdf5', + str(out_h5), + '--backend', + 'hdf5', + '--dtype', + 'float32', + '--compression', + 'gzip', + '--compression-level', + '1', + '--target-chunk-mb', + '1.0', + ] + ) + == 0 + ) assert op.exists(out_h5) # Validate HDF5 contents diff --git a/test/test_voxels_cli.py b/test/test_voxels_cli.py index 895cccb..3b883d7 100644 --- a/test/test_voxels_cli.py +++ b/test/test_voxels_cli.py @@ -71,17 +71,30 @@ def test_convoxel_cli_creates_expected_hdf5(tmp_path, monkeypatch): out_h5 = tmp_path / 'out.h5' monkeypatch.chdir(tmp_path) - assert modelarrayio_main([ - 'nifti-to-h5', - '--group-mask-file', str(group_mask_file), - '--cohort-file', str(cohort_csv), - '--output-hdf5', str(out_h5), - '--backend', 'hdf5', - '--dtype', 'float32', - '--compression', 'gzip', - '--compression-level', '1', - '--target-chunk-mb', '1.0', - ]) == 0 + assert ( + modelarrayio_main( + [ + 'nifti-to-h5', + '--group-mask-file', + str(group_mask_file), + '--cohort-file', + str(cohort_csv), + '--output-hdf5', + str(out_h5), + '--backend', + 'hdf5', + '--dtype', + 'float32', + '--compression', + 'gzip', + '--compression-level', + '1', + '--target-chunk-mb', + '1.0', + ] + ) + == 0 + ) assert op.exists(out_h5) # Validate HDF5 contents From 6c7b06e685778a56fc2c51bf5739701b9d8c376d Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Thu, 26 Mar 2026 11:00:38 -0400 Subject: [PATCH 3/4] Update test_voxels_s3.py --- test/test_voxels_s3.py | 60 ++++++++++++++++++++---------------------- 1 file changed, 28 insertions(+), 32 deletions(-) diff --git a/test/test_voxels_s3.py b/test/test_voxels_s3.py index 548c7d7..5660b13 100644 --- a/test/test_voxels_s3.py +++ b/test/test_voxels_s3.py @@ -9,13 +9,12 @@ import csv import shutil -import sys import h5py import numpy as np import pytest -from modelarrayio.cli.nifti_to_h5 import main as convoxel_main +from modelarrayio.cli.main import main as modelarrayio_main # Four confirmed ABIDE OHSU subjects used as test data OHSU_SUBJECTS = [ @@ -80,30 +79,30 @@ def test_convoxel_s3_parallel(tmp_path, group_mask_path, monkeypatch): out_h5 = tmp_path / 'out.h5' monkeypatch.chdir(tmp_path) monkeypatch.setenv('MODELARRAYIO_S3_ANON', '1') - monkeypatch.setattr( - sys, - 'argv', - [ - 'convoxel', - '--group-mask-file', - 'group_mask.nii.gz', - '--cohort-file', - str(cohort_csv), - '--output-hdf5', - str(out_h5), - '--backend', - 'hdf5', - '--dtype', - 'float32', - '--compression', - 'gzip', - '--compression-level', - '1', - '--s3-workers', - '4', - ], + assert ( + modelarrayio_main( + [ + 'nifti-to-h5', + '--group-mask-file', + 'group_mask.nii.gz', + '--cohort-file', + str(cohort_csv), + '--output-hdf5', + str(out_h5), + '--backend', + 'hdf5', + '--dtype', + 'float32', + '--compression', + 'gzip', + '--compression-level', + '1', + '--s3-workers', + '4', + ] + ) + == 0 ) - assert convoxel_main() == 0 assert out_h5.exists() with h5py.File(out_h5, 'r') as h5: @@ -144,7 +143,7 @@ def test_convoxel_s3_serial_matches_parallel(tmp_path, group_mask_path, monkeypa ) base_argv = [ - 'convoxel', + 'nifti-to-h5', '--group-mask-file', str(group_mask_path), '--cohort-file', @@ -160,12 +159,9 @@ def test_convoxel_s3_serial_matches_parallel(tmp_path, group_mask_path, monkeypa monkeypatch.chdir(tmp_path) monkeypatch.setenv('MODELARRAYIO_S3_ANON', '1') for workers, name in [('1', 'serial.h5'), ('4', 'parallel.h5')]: - monkeypatch.setattr( - sys, - 'argv', - base_argv + ['--output-hdf5', name, '--s3-workers', workers], - ) - assert convoxel_main() == 0, f'convoxel failed (workers={workers})' + assert ( + modelarrayio_main(base_argv + ['--output-hdf5', name, '--s3-workers', workers]) == 0 + ), f'modelarrayio nifti-to-h5 failed (workers={workers})' with ( h5py.File(tmp_path / 'serial.h5', 'r') as s, From 2405afe5f4eeb4331a97ec05dc45939cd77a57b7 Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Thu, 26 Mar 2026 11:02:35 -0400 Subject: [PATCH 4/4] Update usage.rst --- docs/usage.rst | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/usage.rst b/docs/usage.rst index 244bdd0..9a8e942 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -8,9 +8,9 @@ mif-to-h5 ********** .. argparse:: - :ref: modelarrayio.cli.mif_to_h5.get_parser + :ref: modelarrayio.cli.mif_to_h5._parse_mif_to_h5 :prog: modelarrayio mif-to-h5 - :func: get_parser + :func: _parse_mif_to_h5 ********** @@ -18,9 +18,9 @@ nifti-to-h5 ********** .. argparse:: - :ref: modelarrayio.cli.nifti_to_h5.get_parser + :ref: modelarrayio.cli.nifti_to_h5._parse_nifti_to_h5 :prog: modelarrayio nifti-to-h5 - :func: get_parser + :func: _parse_nifti_to_h5 ********** @@ -28,9 +28,9 @@ cifti-to-h5 ********** .. argparse:: - :ref: modelarrayio.cli.cifti_to_h5.get_parser + :ref: modelarrayio.cli.cifti_to_h5._parse_cifti_to_h5 :prog: modelarrayio cifti-to-h5 - :func: get_parser + :func: _parse_cifti_to_h5 ********** @@ -38,18 +38,18 @@ h5-to-mif ********** .. argparse:: - :ref: modelarrayio.cli.h5_to_mif.get_parser + :ref: modelarrayio.cli.h5_to_mif._parse_h5_to_mif :prog: modelarrayio h5-to-mif - :func: get_parser + :func: _parse_h5_to_mif *********** h5-to-nifti *********** .. argparse:: - :ref: modelarrayio.cli.h5_to_nifti.get_parser + :ref: modelarrayio.cli.h5_to_nifti._parse_h5_to_nifti :prog: modelarrayio h5-to-nifti - :func: get_parser + :func: _parse_h5_to_nifti *********** @@ -57,6 +57,6 @@ h5-to-cifti *********** .. argparse:: - :ref: modelarrayio.cli.h5_to_cifti.get_parser + :ref: modelarrayio.cli.h5_to_cifti._parse_h5_to_cifti :prog: modelarrayio h5-to-cifti - :func: get_parser + :func: _parse_h5_to_cifti