Coverage for labnirs2snirf / args.py: 100%

50 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2025-11-28 06:02 +0000

1""" 

2Module for handling command-line arguments. 

3""" 

4 

5import argparse 

6from pathlib import Path 

7from typing import Self 

8 

9from .error import Labnirs2SnirfError 

10 

11 

12class ArgumentError(Labnirs2SnirfError): 

13 """Error indicating invalid command-line arguments.""" 

14 

15 

16def _file_must_exist(path_str: str) -> Path: 

17 """ 

18 Validate that a file path exists. 

19 

20 This function is used as an argparse type validator to ensure that 

21 a provided path points to an existing file. 

22 

23 Parameters 

24 ---------- 

25 path_str : str 

26 String representation of the file path to validate. 

27 

28 Returns 

29 ------- 

30 Path 

31 Validated Path object if the file exists. 

32 

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 

44 

45 

46def _file_must_not_exist(path_str: str) -> Path: 

47 """ 

48 Validate that a file path does not already exist. 

49 

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. 

53 

54 Parameters 

55 ---------- 

56 path_str : str 

57 String representation of the file path to validate. 

58 

59 Returns 

60 ------- 

61 Path 

62 Validated Path object if the path does not exist. 

63 

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 

75 

76 

77def _validate_drop_value(value: str) -> str: 

78 """ 

79 Validate a single drop value for the --drop argument. 

80 

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. 

86 

87 Returns 

88 ------- 

89 str 

90 Validated and normalized (lowercase) drop value. 

91 

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 ) 

106 

107 

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.") 

111 

112 

113class Arguments: 

114 """ 

115 Class to handle configuration and parsing of command-line arguments. 

116 

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 """ 

122 

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 

131 

132 def __init__(self, progname: str | None = __package__): 

133 """ 

134 Initialize the Arguments parser. 

135 

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 # ) 

214 

215 self.parser = parser 

216 

217 def parse(self, args: list[str]) -> Self: 

218 """ 

219 Parse command-line arguments and populate the Arguments object. 

220 

221 Parameters 

222 ---------- 

223 args : list[str] 

224 List of command-line arguments to parse. If empty, shows help. 

225 

226 Returns 

227 ------- 

228 Self 

229 Self with parsed argument values set as attributes. 

230 

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) 

239 

240 # If --log is specified, ensure verbosity is at least 1 

241 if self.log: 

242 self.verbosity = max(self.verbosity, 1) 

243 

244 # Convert drop list to set to avoid duplicates 

245 if self.drop is not None: 

246 self.drop = set(self.drop) 

247 

248 return self 

249 

250 def __str__(self) -> str: 

251 """ 

252 Return a string representation of the Arguments object. 

253 

254 Returns 

255 ------- 

256 str 

257 String showing arguments stored. 

258 """ 

259 return f"Arguments({str(self.__dict__)})" 

260 

261 def __repr__(self) -> str: 

262 """ 

263 Return a detailed string representation of the Arguments object. 

264 

265 Returns 

266 ------- 

267 str 

268 String showing arguments stored. 

269 """ 

270 return f"Arguments({repr(self.__dict__)})"