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
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-11 03:04 +0000
1"""The parser for command line arguments."""
3from argparse import ArgumentDefaultsHelpFormatter, ArgumentParser
4from datetime import UTC, datetime
5from typing import Final
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__
12#: The default argument parser for latexgit executables.
13__DEFAULT_ARGUMENTS: Final[ArgumentParser] = ArgumentParser(
14 add_help=False,
15 formatter_class=ArgumentDefaultsHelpFormatter,
16)
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.
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
30 >>> ap = make_argparser(__file__, "This is a test program.",
31 ... "This is a test.")
32 >>> isinstance(ap, ArgumentParser)
33 True
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>
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>
58 >>> ap = make_argparser(__file__, "This is a test program.",
59 ... "This is a test.", "0.2")
60 >>> isinstance(ap, ArgumentParser)
61 True
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>
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>
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
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'
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'
117 >>> try:
118 ... make_argparser("te", "", "")
119 ... except ValueError as ve:
120 ... print(ve)
121 invalid file='te'.
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
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
135 >>> try:
136 ... make_argparser("Test", "Bla", "")
137 ... except ValueError as ve:
138 ... print(ve)
139 invalid description='Bla'.
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
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
153 >>> try:
154 ... make_argparser("Test", "This is a long test", "epi")
155 ... except ValueError as ve:
156 ... print(ve)
157 invalid epilog='epi'.
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
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}.")
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)
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)
193 return result
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.
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
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
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,'
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
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'
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'
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'
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'
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'
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
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
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'.
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'.
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.
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.
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'.
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.
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.
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
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: ''.
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
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'.
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
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'.
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
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
417 the_epilog = (the_epilog.replace(" ", NBSP)
418 .replace("-", NBDASH).replace("\n", " "))
419 return f"{text}\n\n{the_epilog}"
422def pycommons_argparser(
423 file: str, description: str, epilog: str) -> ArgumentParser:
424 """
425 Create an argument parser with default settings for `pycommons`.
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
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__)