Coverage for labnirs2snirf / args.py: 100%
50 statements
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-28 06:02 +0000
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-28 06:02 +0000
1"""
2Module for handling command-line arguments.
3"""
5import argparse
6from pathlib import Path
7from typing import Self
9from .error import Labnirs2SnirfError
12class ArgumentError(Labnirs2SnirfError):
13 """Error indicating invalid command-line arguments."""
16def _file_must_exist(path_str: str) -> Path:
17 """
18 Validate that a file path exists.
20 This function is used as an argparse type validator to ensure that
21 a provided path points to an existing file.
23 Parameters
24 ----------
25 path_str : str
26 String representation of the file path to validate.
28 Returns
29 -------
30 Path
31 Validated Path object if the file exists.
33 Raises
34 ------
35 ArgumentError
36 If the path is empty, does not exist, or is not a file.
37 """
38 if not path_str:
39 raise ArgumentError("Path must not be empty.")
40 path = Path(path_str)
41 if not path.exists() or not path.is_file():
42 raise ArgumentError(f"File '{path_str}' does not exist.")
43 return path
46def _file_must_not_exist(path_str: str) -> Path:
47 """
48 Validate that a file path does not already exist.
50 This function is used as an argparse type validator to ensure that
51 a provided path does not point to an existing file, preventing accidental
52 overwrites.
54 Parameters
55 ----------
56 path_str : str
57 String representation of the file path to validate.
59 Returns
60 -------
61 Path
62 Validated Path object if the path does not exist.
64 Raises
65 ------
66 ArgumentError
67 If the path is empty or already exists.
68 """
69 if not path_str:
70 raise ArgumentError("Path must not be empty.")
71 path = Path(path_str)
72 if path.exists():
73 raise ArgumentError(f"Path '{path_str}' already exists.")
74 return path
77def _validate_drop_value(value: str) -> str:
78 """
79 Validate a single drop value for the --drop argument.
81 Parameters
82 ----------
83 value : str
84 Drop value to validate. Can be 'HbT', 'HbO', 'HbR' (case insensitive),
85 or a positive integer indicating wavelength.
87 Returns
88 -------
89 str
90 Validated and normalized (lowercase) drop value.
92 Raises
93 ------
94 ArgumentError
95 If the value is not a valid drop type.
96 """
97 value = value.lower().strip()
98 if value.isdecimal() and int(value) > 0:
99 return value
100 if value in {"hbt", "hbo", "hbr"}:
101 return value
102 raise ArgumentError(
103 f"Invalid drop type '{value}'. Must be 'HbT', 'HbO', 'HbR' "
104 f"(case insensitive) or a positive non-zero integer indicating wavelength.",
105 )
108# class _NotImplementedAction(argparse.Action):
109# def __call__(self, parser, namespace, values, option_string=None):
110# raise ArgumentError(f"'{option_string}' option is not implemented yet.")
113class Arguments:
114 """
115 Class to handle configuration and parsing of command-line arguments.
117 Parameters
118 ----------
119 progname : str or None, default=__package__
120 Program name to display in help message. If None, defaults to package name.
121 """
123 parser: argparse.ArgumentParser
124 source_file: Path
125 target_file: Path
126 type: str
127 log: bool
128 verbosity: int
129 locations: Path | None
130 drop: set[str] | None
132 def __init__(self, progname: str | None = __package__):
133 """
134 Initialize the Arguments parser.
136 Parameters
137 ----------
138 progname : str or None, default=__package__
139 Program name to display in help message. If None, defaults to package name.
140 """
141 parser = argparse.ArgumentParser(
142 description="Convert LabNIRS data to SNIRF format.",
143 allow_abbrev=False,
144 prog=progname if progname else "labnirs2snirf",
145 )
146 parser.add_argument(
147 "source_file",
148 help="path to LabNIRS data file (*.txt)",
149 type=_file_must_exist,
150 )
151 parser.add_argument(
152 "target_file",
153 help="path to output file (*.snirf); if not specified, output is written to the current directory as <out.snirf>",
154 nargs="?",
155 default="out.snirf",
156 type=_file_must_not_exist,
157 )
158 parser.add_argument(
159 "--locations",
160 help="Path to file holding probe location data. "
161 "Location files are expected to follow the .sfp format, i.e. "
162 "tab-separated text file with columns: optode name, x, y, and z, "
163 "where x, y, an z are the 3D coordinates of the optode. "
164 "Conflicts with -g.",
165 type=_file_must_exist,
166 )
167 parser.add_argument(
168 "--type",
169 help="Include specific data category only. "
170 "'Raw' includes raw voltage, 'Hb' includes heamoglobin' data, 'all' (default) includes both.",
171 choices=["hb", "raw", "all"],
172 type=str.lower,
173 default="all",
174 )
175 parser.add_argument(
176 "--drop",
177 help="Drop specific data types. "
178 "Possible values: HbT, HbO, HbR, or wavelength integers (e.g. 780). Can be used multiple times.",
179 action="append",
180 type=_validate_drop_value,
181 )
182 parser.add_argument(
183 "-v",
184 "--verbose",
185 action="count",
186 help="Increase verbosity of output, can be used multiple times. One -v for ERROR/WARNING level, "
187 "-vv for INFO level, -vvv for DEBUG level. Combine with --log to redirect log output to file.",
188 default=0,
189 dest="verbosity",
190 )
191 parser.add_argument(
192 "--log",
193 action="store_true",
194 help="Redirects logging to file labnirs2snirf.log in the current directory. "
195 "Logging level is controlled by -v/--verbose. Specifying --log implies -v."
196 "Log messages written to file contain additional information about where messages occurred.",
197 )
198 # parser.add_argument(
199 # "-csv",
200 # "--tasks",
201 # help="(NOT IMPLEMENTED) path to .csv file containing task timings",
202 # type=Path,
203 # action=NotImplementedAction,
204 # # action=FileMustExistAction,
205 # )
206 # parser.add_argument(
207 # "-pat",
208 # "--patient",
209 # help="(NOT IMPLEMENTED) path to .pat file containing patient and study metadata",
210 # type=Path,
211 # action=NotImplementedAction,
212 # # action=FileMustExistAction,
213 # )
215 self.parser = parser
217 def parse(self, args: list[str]) -> Self:
218 """
219 Parse command-line arguments and populate the Arguments object.
221 Parameters
222 ----------
223 args : list[str]
224 List of command-line arguments to parse. If empty, shows help.
226 Returns
227 -------
228 Self
229 Self with parsed argument values set as attributes.
231 Notes
232 -----
233 If --log is specified, verbosity is automatically set to at least 1.
234 Drop values are converted to a set to avoid duplicates.
235 """
236 parser = self.parser
237 del self.parser
238 parser.parse_args(args=args or ["-h"], namespace=self)
240 # If --log is specified, ensure verbosity is at least 1
241 if self.log:
242 self.verbosity = max(self.verbosity, 1)
244 # Convert drop list to set to avoid duplicates
245 if self.drop is not None:
246 self.drop = set(self.drop)
248 return self
250 def __str__(self) -> str:
251 """
252 Return a string representation of the Arguments object.
254 Returns
255 -------
256 str
257 String showing arguments stored.
258 """
259 return f"Arguments({str(self.__dict__)})"
261 def __repr__(self) -> str:
262 """
263 Return a detailed string representation of the Arguments object.
265 Returns
266 -------
267 str
268 String showing arguments stored.
269 """
270 return f"Arguments({repr(self.__dict__)})"