Coverage for pycommons / io / arguments.py: 100%

59 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2025-12-11 03:04 +0000

1"""The parser for command line arguments.""" 

2 

3from argparse import ArgumentDefaultsHelpFormatter, ArgumentParser 

4from datetime import UTC, datetime 

5from typing import Final 

6 

7from pycommons.processes.python import python_command 

8from pycommons.strings.chars import NBDASH, NBSP 

9from pycommons.types import check_int_range 

10from pycommons.version import __version__ 

11 

12#: The default argument parser for latexgit executables. 

13__DEFAULT_ARGUMENTS: Final[ArgumentParser] = ArgumentParser( 

14 add_help=False, 

15 formatter_class=ArgumentDefaultsHelpFormatter, 

16) 

17 

18 

19def make_argparser(file: str, description: str, epilog: str, 

20 version: str | None = None) -> ArgumentParser: 

21 r""" 

22 Create an argument parser with default settings. 

23 

24 :param file: the `__file__` special variable of the calling script 

25 :param description: the description string 

26 :param epilog: the epilogue string 

27 :param version: an optional version string 

28 :returns: the argument parser 

29 

30 >>> ap = make_argparser(__file__, "This is a test program.", 

31 ... "This is a test.") 

32 >>> isinstance(ap, ArgumentParser) 

33 True 

34 

35 >>> from contextlib import redirect_stdout 

36 >>> from io import StringIO 

37 >>> s = StringIO() 

38 >>> with redirect_stdout(s): 

39 ... ap.print_usage() 

40 >>> print(s.getvalue()) 

41 usage: python3 -m pycommons.io.arguments [-h] 

42 <BLANKLINE> 

43 

44 >>> s = StringIO() 

45 >>> with redirect_stdout(s): 

46 ... ap.print_help() 

47 >>> print(s.getvalue()) 

48 usage: python3 -m pycommons.io.arguments [-h] 

49 <BLANKLINE> 

50 This is a test program. 

51 <BLANKLINE> 

52 options: 

53 -h, --help show this help message and exit 

54 <BLANKLINE> 

55 This is a test. 

56 <BLANKLINE> 

57 

58 >>> ap = make_argparser(__file__, "This is a test program.", 

59 ... "This is a test.", "0.2") 

60 >>> isinstance(ap, ArgumentParser) 

61 True 

62 

63 >>> from contextlib import redirect_stdout 

64 >>> from io import StringIO 

65 >>> s = StringIO() 

66 >>> with redirect_stdout(s): 

67 ... ap.print_usage() 

68 >>> print(s.getvalue()) 

69 usage: python3 -m pycommons.io.arguments [-h] [--version] 

70 <BLANKLINE> 

71 

72 >>> s = StringIO() 

73 >>> with redirect_stdout(s): 

74 ... ap.print_help() 

75 >>> print(s.getvalue()) 

76 usage: python3 -m pycommons.io.arguments [-h] [--version] 

77 <BLANKLINE> 

78 This is a test program. 

79 <BLANKLINE> 

80 options: 

81 -h, --help show this help message and exit 

82 --version show program's version number and exit 

83 <BLANKLINE> 

84 This is a test. 

85 <BLANKLINE> 

86 

87 >>> ap = make_argparser(__file__, "This is a test program.", 

88 ... make_epilog("This program computes something", 

89 ... 2022, 2023, "Thomas Weise", 

90 ... url="https://github.com/thomasWeise/pycommons", 

91 ... email="tweise@hfuu.edu.cn")) 

92 >>> s = StringIO() 

93 >>> with redirect_stdout(s): 

94 ... ap.print_help() 

95 >>> v = ('usage: python3 -m pycommons.io.arguments [-h]\n\nThis is ' 

96 ... 'a test program.\n\noptions:\n -h, --help show this help ' 

97 ... 'message and exit\n\nThis program computes something Copyright' 

98 ... '\xa0©\xa02022\u20112023,\xa0Thomas\xa0Weise,\nGNU\xa0GENERAL' 

99 ... '\xa0PUBLIC\xa0LICENSE\xa0Version\xa03,\xa029\xa0June' 

100 ... '\xa02007,\nhttps://github.com/thomasWeise/pycommons, ' 

101 ... 'tweise@hfuu.edu.cn\n') 

102 >>> s.getvalue() == v 

103 True 

104 

105 >>> try: 

106 ... make_argparser(1, "", "") 

107 ... except TypeError as te: 

108 ... print(te) 

109 descriptor '__len__' requires a 'str' object but received a 'int' 

110 

111 >>> try: 

112 ... make_argparser(None, "", "") 

113 ... except TypeError as te: 

114 ... print(te) 

115 descriptor '__len__' requires a 'str' object but received a 'NoneType' 

116 

117 >>> try: 

118 ... make_argparser("te", "", "") 

119 ... except ValueError as ve: 

120 ... print(ve) 

121 invalid file='te'. 

122 

123 >>> try: 

124 ... make_argparser("test", 1, "") 

125 ... except TypeError as te: 

126 ... print(te) 

127 descriptor 'strip' for 'str' objects doesn't apply to a 'int' object 

128 

129 >>> try: 

130 ... make_argparser("Test", None, "") 

131 ... except TypeError as te: 

132 ... print(te) 

133 descriptor 'strip' for 'str' objects doesn't apply to a 'NoneType' object 

134 

135 >>> try: 

136 ... make_argparser("Test", "Bla", "") 

137 ... except ValueError as ve: 

138 ... print(ve) 

139 invalid description='Bla'. 

140 

141 >>> try: 

142 ... make_argparser("Test", "This is a long test", 1) 

143 ... except TypeError as te: 

144 ... print(te) 

145 descriptor 'strip' for 'str' objects doesn't apply to a 'int' object 

146 

147 >>> try: 

148 ... make_argparser("Test", "This is a long test", None) 

149 ... except TypeError as te: 

150 ... print(te) 

151 descriptor 'strip' for 'str' objects doesn't apply to a 'NoneType' object 

152 

153 >>> try: 

154 ... make_argparser("Test", "This is a long test", "epi") 

155 ... except ValueError as ve: 

156 ... print(ve) 

157 invalid epilog='epi'. 

158 

159 >>> try: 

160 ... make_argparser(__file__, "This is a long test", 

161 ... "long long long epilog", 1) 

162 ... except TypeError as te: 

163 ... print(str(te)[:60]) 

164 descriptor 'strip' for 'str' objects doesn't apply to a 'int 

165 

166 >>> try: 

167 ... make_argparser(__file__, "This is a long test", 

168 ... "long long long epilog", " ") 

169 ... except ValueError as ve: 

170 ... print(ve) 

171 Invalid version string ' '. 

172 """ 

173 if str.__len__(file) <= 3: 

174 raise ValueError(f"invalid file={file!r}.") 

175 description = str.strip(description) 

176 if str.__len__(description) <= 12: 

177 raise ValueError(f"invalid description={description!r}.") 

178 epilog = str.strip(epilog) 

179 if str.__len__(epilog) <= 10: 

180 raise ValueError(f"invalid epilog={epilog!r}.") 

181 

182 result: Final[ArgumentParser] = ArgumentParser( 

183 parents=[__DEFAULT_ARGUMENTS], prog=" ".join(python_command(file)), 

184 description=description, epilog=epilog, 

185 formatter_class=__DEFAULT_ARGUMENTS.formatter_class) 

186 

187 if version is not None: 

188 uversion = str.strip(version) 

189 if str.__len__(uversion) < 1: 

190 raise ValueError(f"Invalid version string {version!r}.") 

191 result.add_argument("--version", action="version", version=uversion) 

192 

193 return result 

194 

195 

196def make_epilog( 

197 text: str, copyright_start: int | None = None, 

198 copyright_end: int | None = None, author: str | None = None, 

199 the_license: str | None = 

200 "GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007", 

201 url: str | None = None, 

202 email: str | None = None) -> str: 

203 r""" 

204 Build an epilogue from the given components. 

205 

206 :param text: the epilog text 

207 :param copyright_start: the start year of the copyright, or `None` for no 

208 copyright duration 

209 :param copyright_end: the end year of the copyright, or `None` for using 

210 the current year (unless `copyright_start` is `None`, in which case, 

211 no copyright information is generated). 

212 :param author: the author name, or `None` for no author 

213 :param the_license: the license, or `None` for no license 

214 :param url: the URL, or `None` for no URL 

215 :param email: the email address(es) of the author, or `None` for no email 

216 address information 

217 :returns: the copyright information 

218 

219 >>> cy = datetime.now(tz=UTC).year 

220 >>> ex = (f"This is a test.\n\nGNU\xa0GENERAL\xa0PUBLIC\xa0LICENSE" 

221 ... "\xa0Version\xa03,\xa029\xa0June\xa02007") 

222 >>> make_epilog("This is a test.") == ex 

223 True 

224 

225 >>> make_epilog("This is a test.", 2011, 2030, "Test User", 

226 ... "Test License", "http://testurl", "test@test.com")[:50] 

227 'This is a test.\n\nCopyright\xa0©\xa02011\u20112030,\xa0Test\xa0User,' 

228 

229 >>> ex = (f"This is a test.\n\nCopyright\xa0©\xa02011\u2011{cy}," 

230 ... "\xa0Test\xa0User, Test\xa0License, http://testurl, " 

231 ... "test@test.com") 

232 >>> make_epilog("This is a test.", 2011, None, "Test User", 

233 ... "Test License", "http://testurl", "test@test.com") == ex 

234 True 

235 

236 >>> make_epilog("This is a test.", 2011, 2030, "Test User", 

237 ... "Test License", "http://testurl", "test@test.com")[50:] 

238 ' Test\xa0License, http://testurl, test@test.com' 

239 

240 >>> make_epilog("This is a test.", 2030, 2030, "Test User", 

241 ... "Test License", "http://testurl", "test@test.com")[:50] 

242 'This is a test.\n\nCopyright\xa0©\xa02030,\xa0Test\xa0User, Test' 

243 

244 >>> make_epilog("This is a test.", 2030, 2030, "Test User", 

245 ... "Test License", "http://testurl", "test@test.com")[50:] 

246 '\xa0License, http://testurl, test@test.com' 

247 

248 >>> make_epilog("This is a test.", None, None, "Test User", 

249 ... "Test License", "http://testurl", "test@test.com")[:50] 

250 'This is a test.\n\nTest\xa0User, Test\xa0License, http://t' 

251 

252 >>> make_epilog("This is a test.", None, None, "Test User", 

253 ... "Test License", "http://testurl", "test@test.com")[50:] 

254 'esturl, test@test.com' 

255 

256 >>> try: 

257 ... make_epilog(1, None, None, "Test User", 

258 ... "Test License", "http://testurl", "test@test.com") 

259 ... except TypeError as te: 

260 ... print(te) 

261 descriptor 'strip' for 'str' objects doesn't apply to a 'int' object 

262 

263 >>> try: 

264 ... make_epilog(None, None, None, "Test User", 

265 ... "Test License", "http://testurl", "test@test.com") 

266 ... except TypeError as te: 

267 ... print(te) 

268 descriptor 'strip' for 'str' objects doesn't apply to a 'NoneType' object 

269 

270 >>> try: 

271 ... make_epilog("1", None, None, "Test User", 

272 ... "Test License", "http://testurl", "test@test.com") 

273 ... except ValueError as ve: 

274 ... print(ve) 

275 Epilog text too short: '1'. 

276 

277 >>> try: 

278 ... make_epilog("This is a test.", "v", None, "Test User", 

279 ... "Test License", "http://testurl", "test@test.com") 

280 ... except TypeError as te: 

281 ... print(te) 

282 copyright_start should be an instance of int but is str, namely 'v'. 

283 

284 >>> try: 

285 ... make_epilog("This is a test.", -2, None, "Test User", 

286 ... "Test License", "http://testurl", "test@test.com") 

287 ... except ValueError as ve: 

288 ... print(ve) 

289 copyright_start=-2 is invalid, must be in 1970..2500. 

290 

291 >>> try: 

292 ... make_epilog("This is a test.", 3455334, None, "Test User", 

293 ... "Test License", "http://testurl", "test@test.com") 

294 ... except ValueError as ve: 

295 ... print(ve) 

296 copyright_start=3455334 is invalid, must be in 1970..2500. 

297 

298 >>> try: 

299 ... make_epilog("This is a test.", 2002, "v", "Test User", 

300 ... "Test License", "http://testurl", "test@test.com") 

301 ... except TypeError as te: 

302 ... print(te) 

303 copyright_end should be an instance of int but is str, namely 'v'. 

304 

305 >>> try: 

306 ... make_epilog("This is a test.", 2002, 12, "Test User", 

307 ... "Test License", "http://testurl", "test@test.com") 

308 ... except ValueError as ve: 

309 ... print(ve) 

310 copyright_end=12 is invalid, must be in 2002..2500. 

311 

312 >>> try: 

313 ... make_epilog("This is a test.", 2023, 3455334, "Test User", 

314 ... "Test License", "http://testurl", "test@test.com") 

315 ... except ValueError as ve: 

316 ... print(ve) 

317 copyright_end=3455334 is invalid, must be in 2023..2500. 

318 

319 >>> try: 

320 ... make_epilog("This is a test.", None, None, 2, 

321 ... "Test License", "http://testurl", "test@test.com") 

322 ... except TypeError as te: 

323 ... print(te) 

324 descriptor 'strip' for 'str' objects doesn't apply to a 'int' object 

325 

326 >>> try: 

327 ... make_epilog("This is a test.", None, None, "", 

328 ... "Test License", "http://testurl", "test@test.com") 

329 ... except ValueError as ve: 

330 ... print(ve) 

331 Author too short: ''. 

332 

333 >>> try: 

334 ... make_epilog("This is a test.", None, None, "Tester", 

335 ... 23, "http://testurl", "test@test.com") 

336 ... except TypeError as te: 

337 ... print(te) 

338 descriptor 'strip' for 'str' objects doesn't apply to a 'int' object 

339 

340 >>> try: 

341 ... make_epilog("This is a test.", None, None, "Tester", 

342 ... "Te", "http://testurl", "test@test.com") 

343 ... except ValueError as ve: 

344 ... print(ve) 

345 License too short: 'Te'. 

346 

347 >>> try: 

348 ... make_epilog("This is a test.", None, None, "Tester", 

349 ... "GPL", 2, "test@test.com") 

350 ... except TypeError as te: 

351 ... print(te) 

352 descriptor 'strip' for 'str' objects doesn't apply to a 'int' object 

353 

354 >>> try: 

355 ... make_epilog("This is a test.", None, None, "Tester", 

356 ... "GPL", "http", "test@test.com") 

357 ... except ValueError as ve: 

358 ... print(ve) 

359 Url too short: 'http'. 

360 

361 >>> try: 

362 ... make_epilog("This is a test.", None, None, "Tester", 

363 ... "GPL", "http://www.test.com", 1) 

364 ... except TypeError as te: 

365 ... print(te) 

366 descriptor 'strip' for 'str' objects doesn't apply to a 'int' object 

367 

368 >>> try: 

369 ... make_epilog("This is a test.", None, None, "Tester", 

370 ... "GPL", "http://www.test.com", "a@b") 

371 ... except ValueError as ve: 

372 ... print(ve) 

373 Email too short: 'a@b'. 

374 """ 

375 text = str.strip(text) 

376 if str.__len__(text) <= 10: 

377 raise ValueError(f"Epilog text too short: {text!r}.") 

378 the_epilog: str = "" 

379 if copyright_start is not None: 

380 copyright_start = check_int_range( 

381 copyright_start, "copyright_start", 1970, 2500) 

382 if copyright_end is None: 

383 copyright_end = check_int_range( 

384 datetime.now(tz=UTC).year, 

385 "year", 1970, 2500) 

386 else: 

387 copyright_end = check_int_range( 

388 copyright_end, "copyright_end", copyright_start, 2500) 

389 the_epilog = f"Copyright \u00a9 {copyright_start}" \ 

390 if copyright_start >= copyright_end \ 

391 else f"Copyright \u00a9 {copyright_start}-{copyright_end}" 

392 if author is not None: 

393 author = str.strip(author) 

394 if str.__len__(author) < 1: 

395 raise ValueError(f"Author too short: {author!r}.") 

396 the_epilog = f"{the_epilog}, {author}" \ 

397 if str.__len__(the_epilog) > 0 else author 

398 if the_license is not None: 

399 the_license = str.strip(the_license) 

400 if str.__len__(the_license) < 3: 

401 raise ValueError(f"License too short: {the_license!r}.") 

402 the_epilog = f"{the_epilog},\n{the_license}" \ 

403 if str.__len__(the_epilog) > 0 else the_license 

404 if url is not None: 

405 url = str.strip(url) 

406 if str.__len__(url) < 6: 

407 raise ValueError(f"Url too short: {url!r}.") 

408 the_epilog = f"{the_epilog},\n{url}" \ 

409 if str.__len__(the_epilog) > 0 else url 

410 if email is not None: 

411 email = str.strip(email) 

412 if str.__len__(email) < 5: 

413 raise ValueError(f"Email too short: {email!r}.") 

414 the_epilog = f"{the_epilog},\n{email}" \ 

415 if str.__len__(the_epilog) > 0 else email 

416 

417 the_epilog = (the_epilog.replace(" ", NBSP) 

418 .replace("-", NBDASH).replace("\n", " ")) 

419 return f"{text}\n\n{the_epilog}" 

420 

421 

422def pycommons_argparser( 

423 file: str, description: str, epilog: str) -> ArgumentParser: 

424 """ 

425 Create an argument parser with default settings for `pycommons`. 

426 

427 :param file: the `__file__` special variable of the calling script 

428 :param description: the description string 

429 :param epilog: the epilogue string 

430 :returns: the argument parser 

431 

432 >>> ap = pycommons_argparser( 

433 ... __file__, "This is a test program.", "This is a test.") 

434 >>> isinstance(ap, ArgumentParser) 

435 True 

436 >>> "Copyright" in ap.epilog 

437 True 

438 """ 

439 return make_argparser( 

440 file, description, 

441 make_epilog(epilog, 2023, 2024, "Thomas Weise", 

442 url="https://thomasweise.github.io/pycommons", 

443 email="tweise@hfuu.edu.cn, tweise@ustc.edu.cn"), 

444 __version__)