oceanproc 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. oceanproc/__init__.py +1 -0
  2. oceanproc/bids_wrapper.py +331 -0
  3. oceanproc/events_long.py +153 -0
  4. oceanproc/firstlevel/__init__.py +0 -0
  5. oceanproc/firstlevel/run_glm.py +923 -0
  6. oceanproc/firstlevel/tests/__init__.py +0 -0
  7. oceanproc/firstlevel/tests/test_run_glm.py +165 -0
  8. oceanproc/fmriprep_wrapper.py +222 -0
  9. oceanproc/group_series.py +228 -0
  10. oceanproc/grouplevel/__init__.py +0 -0
  11. oceanproc/grouplevel/computations.py +80 -0
  12. oceanproc/grouplevel/design_mat_utils.py +54 -0
  13. oceanproc/grouplevel/dscalar_utils.py +51 -0
  14. oceanproc/grouplevel/run.py +18 -0
  15. oceanproc/grouplevel/utils.py +4 -0
  16. oceanproc/ocean_proc.py +215 -0
  17. oceanproc/oceanparse.py +107 -0
  18. oceanproc/utils.py +224 -0
  19. oceanproc/web/TODO.md +4 -0
  20. oceanproc/web/__init__.py +0 -0
  21. oceanproc/web/app.py +78 -0
  22. oceanproc/web/config.json +7 -0
  23. oceanproc/web/get_parser_json.py +58 -0
  24. oceanproc/web/static/components/ArgparseForm.js +9 -0
  25. oceanproc/web/static/components/ArgparseFormEntry.js +52 -0
  26. oceanproc/web/static/components/DirIcon.js +11 -0
  27. oceanproc/web/static/components/FileBrowser.js +85 -0
  28. oceanproc/web/static/components/FileBrowserOpenBtn.js +12 -0
  29. oceanproc/web/static/components/FileEntry.js +32 -0
  30. oceanproc/web/static/components/FileIcon.js +11 -0
  31. oceanproc/web/static/components/OceanprocForm.js +53 -0
  32. oceanproc/web/static/components/styles.css +44 -0
  33. oceanproc/web/templates/filesystem.html +13 -0
  34. oceanproc/web/templates/filesystem_fail.html +2 -0
  35. oceanproc/web/templates/oceanproc_form.html +23 -0
  36. oceanproc/web/templates/react-test.html +0 -0
  37. oceanproc/web/templates/username_card_component_test.html +39 -0
  38. oceanproc-0.1.0.dist-info/METADATA +29 -0
  39. oceanproc-0.1.0.dist-info/RECORD +41 -0
  40. oceanproc-0.1.0.dist-info/WHEEL +4 -0
  41. oceanproc-0.1.0.dist-info/entry_points.txt +4 -0
@@ -0,0 +1,80 @@
1
+ from numba import jit
2
+ import torch
3
+ import nibabel as nib
4
+ import numpy as np
5
+ import scipy
6
+ from .utils import set_device
7
+
8
+ # @jit(nopython=True)
9
+ # def ols(y, x):
10
+ # if len(y.shape) != 2:
11
+ # raise ValueError(f"Shape of random variable vector y must be of length 2. y.shape: {y.shape}")
12
+ # elif len(x.shape) != 2:
13
+ # raise ValueError(f"Shape of design matrix x must be of length 2. x.shape: {x.shape}")
14
+ # elif y.shape[0] != x.shape[0]:
15
+ # raise ValueError(f"The number of rows in y and x must match.\ny rows: {y.shape[0]}\nx rows: {x.shape[0]}")
16
+ # beta_hat = np.linalg.inv(np.transpose(x) @ x) @ np.transpose(x) @ y
17
+ # residuals = y - (x @ beta_hat)
18
+ # return (beta_hat, residuals)
19
+
20
+
21
+ def ols_regression_over_dscalars(combined_dscalars: nib.cifti2.cifti2.Cifti2Image,
22
+ design_mat: np.ndarray|torch.Tensor):
23
+ device = set_device()
24
+ y = torch.from_numpy(combined_dscalars.get_fdata(), device=device)
25
+ if isinstance(design_mat, np.ndarray):
26
+ x = torch.from_numpy(design_mat)
27
+ if not torch.is_tensor(x):
28
+ raise TypeError("design_mat must be a torch tensor or numpy ndarray")
29
+ x = x.to(device)
30
+ beta_hat = torch.linalg.pinv(torch.transpose(x, 0, 1) @ x) @ torch.transpose(x, 0, 1) @ y
31
+ residuals = y - (x @ beta_hat)
32
+ return (beta_hat, residuals)
33
+
34
+
35
+ def ttest_1samp_over_dscalars(combined_dscalars: nib.cifti2.cifti2.Cifti2Image,
36
+ output_path: str,
37
+ popmean: np.ndarray,
38
+ alpha: float = 0.05,
39
+ tails: int = 1):
40
+ assert tails in (1,2), "tails must be either 1 or 2"
41
+ assert alpha > 0 and alpha < 1, "alpha must be between 0 and 1"
42
+ device = set_device()
43
+ popmean = torch.from_numpy(popmean, device=device)
44
+ data = torch.from_numpy(combined_dscalars.get_fdata(), device=device)
45
+ n = data.shape[0]
46
+ df = n -1
47
+ sample_means = torch.mean(data, 0)
48
+ mean_differences = sample_means - popmean
49
+ standard_error = torch.std(data, 0) / (n**0.5)
50
+ t_mat = mean_differences / standard_error
51
+ t_mat = t_mat.to("cpu")
52
+ p_mat = tails * scipy.stats.t.sf(t_mat, 30)
53
+ img = combined_dscalars.__class__(p_mat, combined_dscalars.header)
54
+ nib.save(img, output_path)
55
+
56
+
57
+ # @jit(nopython=True)
58
+ # def ttest_1samp_over_dscalars(combined_dscalars: nib.cifti2.cifti2.Cifti2Image,
59
+ # output_path: str,
60
+ # popmean: float = 0,
61
+ # alpha: float = 0.05,
62
+ # tails: int = 1):
63
+ # assert tails in (1,2), "tails must be either 1 or 2"
64
+ # assert alpha > 0 and alpha < 1, "alpha must be between 0 and 1"
65
+ # n = combined_dscalars.shape[0]
66
+ # df = n - 1
67
+ # sample_means = np.mean(combined_dscalars, axis=0)
68
+ # sample_error = (np.std(combined_dscalars, axis=0)) / np.sqrt(n)
69
+ # diff_mean = sample_means - popmean
70
+ # t_scores = diff_mean / sample_error
71
+ # p_values = tails * (1 - scipy.stats.t.cdf(abs(t_scores), df))
72
+ # p_values = np.array([p_values])
73
+ # header = nib.cifti2.cifti2.Cifti2Header(p_values)
74
+ # img = nib.cifti2.cifti2.Cifti2Image(dataobj=p_values, header=header)
75
+ # nib.save(img, output_path)
76
+
77
+
78
+
79
+
80
+
@@ -0,0 +1,54 @@
1
+ import pandas as pd
2
+ import numpy as np
3
+ import re
4
+ from pathlib import Path
5
+
6
+ def _read_design_matrix(path: str,
7
+ sep: str = None,
8
+ dtype: type = None) -> np.typing.ArrayLike:
9
+ if sep is None and re.match(r'^.*\.csv$', path):
10
+ sep = ','
11
+ elif sep is None and re.match(r'^.*\.tsv$', path):
12
+ sep = '\t'
13
+ if sep is None:
14
+ raise ValueError("Unrecognized separator in design matrix file (should either be a .csv or .tsv file)")
15
+ df = pd.DataFrame(path, sep=sep)
16
+ return df.to_numpy()
17
+
18
+
19
+ def design_helper(bids_path: Path, output_file: Path):
20
+ assert bids_path.exists() and bids_path.is_dir(), "The supplied BIDS Path must be an existing directory"
21
+ assert output_file.suffix == ".csv", "The supplied output file must be of type '.csv'"
22
+ entry_list = []
23
+ subs = sorted([d for d in bids_path.glob("sub-*") if d.is_dir()])
24
+ for sub_dir in subs:
25
+ ses_dirs = sorted([d for d in sub_dir.glob("ses-*") if d.is_dir()])
26
+ for ses_dir in ses_dirs:
27
+ beta_files = sorted([f for f in ses_dir.glob("func/sub-*_ses-*_task-*_desc-*model-*-beta-*-frame-*.nii*") if f.is_file()])
28
+ for file in beta_files:
29
+ name = file.name
30
+ underscore_split = name.split("_")
31
+ sub = underscore_split[0].split("-")[-1]
32
+ ses = underscore_split[1].split("-")[-1]
33
+ task = underscore_split[2].split("-")[-1]
34
+ desc_split = underscore_split[4].split("-")
35
+ model = desc_split[desc_split.index("model") + 1]
36
+ beta = desc_split[desc_split.index("beta") + 1]
37
+ frame = desc_split[desc_split.index("frame") + 1].split(".")[0]
38
+ file_type = "surface" if name.split(".")[1] == "dscalar" else "volume"
39
+ entry_list.append(
40
+ {
41
+ "sub" : sub,
42
+ "ses" : ses,
43
+ "task" : task,
44
+ "model" : model,
45
+ "beta" : beta,
46
+ "frame" : frame,
47
+ "file_type" : file_type,
48
+ "path" : str(file.resolve())
49
+ }
50
+ )
51
+ design_scaffold = pd.DataFrame(entry_list)
52
+ design_scaffold.to_csv(output_file, index=False)
53
+
54
+
@@ -0,0 +1,51 @@
1
+ import re
2
+ import os
3
+ from glob import glob
4
+ from pathlib import Path
5
+ import nibabel as nib
6
+ import numpy as np
7
+
8
+
9
+ def _find_all_dscalars(func_dir: str|Path) -> list[str]:
10
+ """
11
+ Return a list of all paths to dscalars in func_dir.
12
+ :param func_dir: path to a subject's functional derivatives directory
13
+ :type func_dir: pathlib.Path or str
14
+ :return: a list of absolute paths to dscalars
15
+ :rtype: list[str]
16
+ """
17
+ rel_paths = glob(os.path.join(func_dir, "*dscalar.nii"))
18
+ return [os.path.abspath(p) for p in rel_paths]
19
+
20
+ def _filter_dscalar_paths_by_regressor(dscalar_paths: list[str],
21
+ regressors: list[str]) -> list[str]:
22
+ """
23
+ Returns a list of paths to .dscalar.nii files containing the names of given regressors.
24
+ :param dscalar_paths: list of absolute paths to a set of dscalar files
25
+ :type dscalar_paths: list[str]
26
+ :param regressors: list of names of regressors contained in the dscalar base name
27
+ :type regressors: list[str]
28
+ """
29
+ if len(regressors) < 1:
30
+ raise ValueError("List of regressor names must not be empty.")
31
+ filtered_dscalars = []
32
+ for regressor in regressors:
33
+ filtered_dscalars.extend([p for p in dscalar_paths if re.search(regressor, os.path.basename(p))])
34
+ return filtered_dscalars
35
+
36
+ def _load_dscalars(dscalar_paths: list[str]) -> list[nib.cifti2.cifti2.Cifti2Image]:
37
+ assert len(dscalar_paths) > 0, "Lenght of dscalar paths list must be greater than 0"
38
+ dscalars = []
39
+ for p in dscalar_paths:
40
+ dscalars.append(nib.load(p))
41
+ return dscalars
42
+
43
+ def _combine_dscalars(dscalars: list[nib.cifti2.cifti2.Cifti2Image]) -> np.typing.ArrayLike:
44
+ assert len(dscalars) > 0, "Length of dscalars list must be greater than 0"
45
+ header = dscalars[0].header
46
+ combined_data = np.array([img.get_fdata().squeeze() for img in dscalars])
47
+ scalar_axis = nib.cifti2.cifti2_axes.ScalarAxis([f"betamap{i+1}" for i in range(combined_data.shape[0])])
48
+ brain_model_axis = header.get_axis(1)
49
+ combined_img = nib.Cifti2Image(combined_data, (scalar_axis, brain_model_axis))
50
+ return combined_img
51
+
@@ -0,0 +1,18 @@
1
+ import pathlib
2
+ import os
3
+ from glob import glob
4
+
5
+
6
+ def find_beta_maps(output_dir: pathlib.Path|str,
7
+ sub: str):
8
+ beta_map_paths = []
9
+ for beta_map in glob(
10
+ os.path.join(
11
+ output_dir,
12
+ f"sub-{sub}",
13
+ "func",
14
+ "*desc-*activation*"
15
+ )
16
+ ):
17
+ beta_map_paths.append(beta_map)
18
+ return beta_map_paths
@@ -0,0 +1,4 @@
1
+ import torch
2
+
3
+ def set_device():
4
+ return torch.device('cuda' if torch.cuda.is_available() else 'cpu')
@@ -0,0 +1,215 @@
1
+ #!/usr/bin/env python3
2
+
3
+ import argparse
4
+ import sys
5
+ from pathlib import Path
6
+ import logging
7
+ import datetime
8
+ from .bids_wrapper import dicom_to_bids
9
+ from .group_series import map_fmap_to_func, map_fmap_to_func_with_pairing_file
10
+ from .fmriprep_wrapper import process_data
11
+ from .events_long import create_events_and_confounds
12
+ from .utils import exit_program_early, prompt_user_continue, default_log_format, add_file_handler, export_args_to_file, flags, debug_logging, log_linebreak
13
+ from .oceanparse import OceanParser
14
+ import shlex
15
+ import shutil
16
+ from subprocess import Popen, PIPE
17
+ from textwrap import dedent
18
+
19
+ logging.basicConfig(level=logging.INFO,
20
+ handlers=[logging.StreamHandler(stream=sys.stdout)],
21
+ format=default_log_format)
22
+ logger = logging.getLogger()
23
+
24
+ @debug_logging
25
+ def make_work_directory(dir_path:Path, subject:str, session:str) -> Path:
26
+ dir_to_make = dir_path / f"sub-{subject}_ses-{session}"
27
+ if dir_to_make.exists():
28
+ want_to_delete = prompt_user_continue(dedent("""
29
+ A work directory already exists for this subject and session.
30
+ Would you like to delete its contents and start fresh?
31
+ """))
32
+ if want_to_delete:
33
+ logger.debug("removing the old working directory and its contents")
34
+ shutil.rmtree(dir_to_make)
35
+ else:
36
+ return dir_to_make
37
+ dir_to_make.mkdir()
38
+ logger.info(f"creating a new working directory at the path: {dir_to_make}")
39
+ return dir_to_make
40
+
41
+
42
+ def main():
43
+ parser = OceanParser(
44
+ prog="oceanproc",
45
+ description="Ocean Labs adult MRI preprocessing",
46
+ exit_on_error=False,
47
+ fromfile_prefix_chars="@",
48
+ epilog="An arguments file can be accepted with @FILEPATH"
49
+ )
50
+ session_arguments = parser.add_argument_group("Session Specific")
51
+ config_arguments = parser.add_argument_group("Configuration Arguments", "These arguments are saved to a file if the '--export_args' option is used")
52
+
53
+ session_arguments.add_argument("--subject", "-su", required=True,
54
+ help="The identifier of the subject to preprocess")
55
+ session_arguments.add_argument("--session", "-se", required=True,
56
+ help="The identifier of the session to preprocess")
57
+ session_arguments.add_argument("--source_data", "-sd", type=Path, required=True,
58
+ help="The path to the directory containing the raw DICOM files for this subject and session")
59
+ session_arguments.add_argument("--xml_path", "-x", type=Path, required=True,
60
+ help="The path to the xml file for this subject and session")
61
+ session_arguments.add_argument("--skip_dcm2bids", action="store_true",
62
+ help="Flag to indicate that dcm2bids does not need to be run for this subject and session")
63
+ session_arguments.add_argument("--skip_fmap_pairing", action="store_true",
64
+ help="Flag to indicate that the pairing of fieldmaps to BOLD runs does not need to be performed for this subject and session")
65
+ session_arguments.add_argument("--skip_fmriprep", action="store_true",
66
+ help="Flag to indicate that fMRIPrep does not need to be run for this subject and session")
67
+ session_arguments.add_argument("--skip_event_files", action="store_true",
68
+ help="Flag to indicate that the making of a long formatted events file is not needed for the subject and session")
69
+ session_arguments.add_argument("--export_args", "-ea", type=Path,
70
+ help="Path to a file to save the current configuration arguments")
71
+ session_arguments.add_argument("--keep_work_dir", action="store_true",
72
+ help="Flag to stop the deletion of the fMRIPrep working directory")
73
+ session_arguments.add_argument("--debug", action="store_true",
74
+ help="Flag to run the program in debug mode for more verbose logging")
75
+ session_arguments.add_argument("--fmap_pairing_file",
76
+ help="Path to JSON containing info on how to pair fieldmaps to BOLD runs.",
77
+ type=Path)
78
+
79
+ config_arguments.add_argument("--bids_path", "-b", type=Path, required=True,
80
+ help="The path to the directory containing the raw nifti data for all subjects, in BIDS format")
81
+ config_arguments.add_argument("--derivs_path", "-d", type=Path, required=True,
82
+ help="The path to the BIDS formated derivatives directory for this subject")
83
+ config_arguments.add_argument("--derivs_subfolder", "-ds", default="fmriprep",
84
+ help="The subfolder in the derivatives directory where bids style outputs should be stored. The default is 'fmriprep'.")
85
+ config_arguments.add_argument("--bids_config", "-c", type=Path, required=True,
86
+ help="The path to the dcm2bids config file to use for this subject and session")
87
+ config_arguments.add_argument("--nordic_config", "-n", type=Path,
88
+ help="The path to the second dcm2bids config file to use for this subject and session. This implies that the session contains NORDIC data")
89
+ config_arguments.add_argument("--nifti", action=argparse.BooleanOptionalAction,
90
+ help="Flag to specify that the source directory contains files of type NIFTI (.nii/.jsons) instead of DICOM")
91
+ config_arguments.add_argument("--anat_only", action='store_true',
92
+ help="Flag to specify only anatomical images should be processed.")
93
+ config_arguments.add_argument("--fd_spike_threshold", "-fd", type=float, default=0.9,
94
+ help="framewise displacement threshold (in mm) to determine outlier framee (Default is 0.9).")
95
+ config_arguments.add_argument("--skip_bids_validation", action="store_true",
96
+ help="Specifies skipping BIDS validation (only enabled for fMRIprep step)")
97
+ config_arguments.add_argument("--fs_subjects_dir", "-fs", type=Path,
98
+ help="The path to the directory that contains previous FreeSurfer outputs/derivatives to use for fMRIPrep. If empty, this is the path where new FreeSurfer outputs will be stored.")
99
+ config_arguments.add_argument("--work_dir", "-w", type=Path, required=True,
100
+ help="The path to the working directory used to store intermediate files")
101
+ config_arguments.add_argument("--fs_license", "-l", type=Path, required=True,
102
+ help="The path to the license file for the local installation of FreeSurfer")
103
+ config_arguments.add_argument("--fmriprep_version", "-fv", default="23.1.4", dest="image",
104
+ help="The version of fmriprep to use. The default is 23.1.4. It is reccomended that an entire study use the same version.")
105
+ args = parser.parse_args()
106
+
107
+ try:
108
+ assert args.derivs_path.is_dir(), "Derivatives directory must exist but it cannot be found"
109
+ assert args.bids_path.is_dir(), "Raw Bids directory must exist but it cannot be found"
110
+ assert args.source_data.is_dir(), "Source data directory must exist but it cannot be found"
111
+ assert args.work_dir.is_dir(), "Work directroy must exist but it cannot be found"
112
+ except AssertionError as e:
113
+ logger.exception(e)
114
+ exit_program_early(e)
115
+
116
+ ##### Export the current configuration arguments to a file #####
117
+ if args.export_args:
118
+ try:
119
+ assert args.export_args.parent.exists() and args.export_args.suffix, "Argument export path must be a file path in a directory that exists"
120
+ logger.info(f"####### Exporting Configuration Arguments to: '{args.export_args}' #######")
121
+ export_args_to_file(args, config_arguments, args.export_args)
122
+ except Exception as e:
123
+ logger.exception(e)
124
+ exit_program_early(e)
125
+
126
+ args.image = f"nipreps/fmriprep:{args.image}"
127
+
128
+ args.derivs_path = args.derivs_path / args.derivs_subfolder
129
+
130
+ log_dir = args.derivs_path / f"sub-{args.subject}/log"
131
+ log_dir.mkdir(parents=True, exist_ok=True)
132
+ log_path = log_dir / f"sub-{args.subject}_ses-{args.session}_oceanproc_desc-{datetime.datetime.now().strftime('%m-%d-%y_%I-%M%p')}.log"
133
+ add_file_handler(logger, log_path)
134
+
135
+ if args.debug:
136
+ flags.debug = True
137
+ logger.setLevel(logging.DEBUG)
138
+
139
+ logger.info("Starting oceanproc...")
140
+ logger.info(f"Log will be stored at {log_path}")
141
+
142
+ # log the input arguments
143
+ for k,v in (dict(args._get_kwargs())).items():
144
+ logger.info(f" {k} : {v}")
145
+
146
+ args.work_dir = make_work_directory(dir_path=args.work_dir,
147
+ subject=args.subject,
148
+ session=args.session)
149
+
150
+ ##### Convert raw DICOMs to BIDS structure #####
151
+ if not args.skip_dcm2bids:
152
+ dicom_to_bids(
153
+ subject=args.subject,
154
+ session=args.session,
155
+ source_dir=args.source_data,
156
+ bids_dir=args.bids_path,
157
+ xml_path=args.xml_path,
158
+ bids_config=args.bids_config,
159
+ nordic_config=args.nordic_config,
160
+ nifti=args.nifti
161
+ )
162
+
163
+ ##### Pair field maps to functional runs #####
164
+ bids_session_dir = args.bids_path / f"sub-{args.subject}/ses-{args.session}"
165
+
166
+ if not args.anat_only and not args.skip_fmap_pairing:
167
+ if args.fmap_pairing_file:
168
+ map_fmap_to_func_with_pairing_file(
169
+ bids_session_dir,
170
+ args.fmap_pairing_file
171
+ )
172
+ else:
173
+ map_fmap_to_func(
174
+ xml_path=args.xml_path,
175
+ bids_dir_path=bids_session_dir
176
+ )
177
+
178
+
179
+ ##### Run fMRIPrep #####
180
+ all_opts = dict(args._get_kwargs())
181
+ fmrip_options = {"work_dir",
182
+ "fs_license",
183
+ "fs_subjects_dir",
184
+ "skip_bids_validation",
185
+ "fd_spike_threshold",
186
+ "anat_only",
187
+ "image"}
188
+
189
+ if not args.skip_fmriprep:
190
+ process_data(
191
+ subject=args.subject,
192
+ session=args.session,
193
+ bids_path=args.bids_path,
194
+ derivs_path=args.derivs_path,
195
+ remove_work_folder=None if args.keep_work_dir else args.work_dir,
196
+ **{o:all_opts[o] for o in fmrip_options}
197
+ )
198
+
199
+
200
+ ##### Create long formatted event files #####
201
+ if not args.skip_event_files:
202
+ create_events_and_confounds(
203
+ bids_path=args.bids_path,
204
+ derivs_path=args.derivs_path,
205
+ sub=args.subject,
206
+ ses=args.session,
207
+ fd_thresh=args.fd_spike_threshold
208
+ )
209
+
210
+ log_linebreak()
211
+ logger.info("####### [DONE] Finished all processing, exiting now #######")
212
+
213
+
214
+ if __name__ == "__main__":
215
+ main()
@@ -0,0 +1,107 @@
1
+ import argparse
2
+ import json
3
+ import sys
4
+
5
+
6
+
7
+ class OceanParser(argparse.ArgumentParser):
8
+
9
+ def convert_arg_line_to_args(self, arg_line: str) -> list[str]:
10
+ """
11
+ Overridded method of argparse.ArgumentParser class.
12
+
13
+ Enables the ability to list the argument flag and its value on the same line
14
+ when reading arguments from a file. Multiple arguments and their values can
15
+ also be on the same line.
16
+
17
+ :param arg_line: a line of text read from a file
18
+ :type arg_line: str
19
+ :return: list of arguments and their values parsed from the 'arg_line' argument
20
+ :rtype: list[str]
21
+ """
22
+ return arg_line.strip().split()
23
+ # return super().convert_arg_line_to_args(arg_line)
24
+
25
+
26
+ def _read_args_from_files(self, arg_strings:list[str]):
27
+ """
28
+ Overridded method of argparse.ArgumentParser class.
29
+
30
+ Reads in each argument string from the input list and checks if any
31
+ string is a reference to an argument-file by checking if the value of
32
+ the first character is equal to self.fromfile_prefix_chars. If so,
33
+ attempts to open this string as a file, and read its contents into the
34
+ the new_arg_strings list as new arguments. The abilities to read in JSON
35
+ files, along with file arguments being prepended to the list have been
36
+ added to this function
37
+
38
+ :param arg_strings: as list of arguments read in from the command line
39
+ :type arg_strings: list[str]
40
+ :return: list of old and new (if any) arguments and their values
41
+ :rtype: list[str]
42
+ """
43
+ # expand arguments referencing files
44
+ new_arg_strings = []
45
+ for arg_string in arg_strings:
46
+
47
+ # for regular arguments, just add them back into the list
48
+ if not arg_string or arg_string[0] not in self.fromfile_prefix_chars:
49
+ new_arg_strings.append(arg_string)
50
+
51
+ # replace arguments referencing files with the file content
52
+ else:
53
+ try:
54
+ with open(arg_string[1:]) as args_file:
55
+ file_arg_strings = []
56
+ if arg_string.endswith(".json"):
57
+ jd = json.load(args_file)
58
+ for k,v in jd.items():
59
+ file_arg_strings.append(k)
60
+ if isinstance(v, list):
61
+ for val in v:
62
+ file_arg_strings.append(str(val))
63
+ elif isinstance(v, str):
64
+ if not v or not v.strip():
65
+ continue
66
+ else:
67
+ file_arg_strings.append(v)
68
+ else:
69
+ file_arg_strings.append(str(v))
70
+ else:
71
+ for arg_line in args_file.read().splitlines():
72
+ for arg in self.convert_arg_line_to_args(arg_line):
73
+ file_arg_strings.append(arg)
74
+ file_arg_strings = self._read_args_from_files(file_arg_strings)
75
+ new_arg_strings = file_arg_strings + new_arg_strings
76
+ except OSError:
77
+ err = sys.exc_info()[1]
78
+ self.error(str(err))
79
+ # return the modified argument list
80
+ return new_arg_strings
81
+
82
+ def print_usage(self, file=None):
83
+ """
84
+ Overridded method of argparse.ArgumentParser class.
85
+
86
+ Prints the usage message for the argparse.ArgumentParser object.
87
+ An example format for an arguments file input as been appended
88
+
89
+ :param file: a file to print this usage message to (defaults to sys.stdout if None)
90
+ :type file: IO[str]
91
+ """
92
+ if file is None:
93
+ file = sys.stdout
94
+ self._print_message(self.format_usage().rstrip("\n") + " @[ARG_FILE]\n",
95
+ file)
96
+
97
+ class NoOverride(argparse.Action):
98
+ """
99
+ Custom class that inherits from the argparse.Action class.
100
+
101
+ Creates a custom action for an argument that doesn't allow namespace
102
+ values to be overridden by later inputs
103
+ """
104
+ def __call__(self, parser, namespace, values, option_string=None):
105
+ breakpoint()
106
+ if getattr(namespace, self.dest, None) == None:
107
+ setattr(namespace, self.dest, values)