"""The parser for command line arguments."""
from argparse import ArgumentDefaultsHelpFormatter, ArgumentParser
from datetime import datetime, timezone
from typing import Final
from pycommons.processes.python import python_command
from pycommons.strings.chars import NBDASH, NBSP
from pycommons.types import check_int_range
from pycommons.version import __version__
#: The default argument parser for latexgit executables.
__DEFAULT_ARGUMENTS: Final[ArgumentParser] = ArgumentParser(
add_help=False,
formatter_class=ArgumentDefaultsHelpFormatter,
)
[docs]
def make_argparser(file: str, description: str, epilog: str,
version: str | None = None) -> ArgumentParser:
r"""
Create an argument parser with default settings.
:param file: the `__file__` special variable of the calling script
:param description: the description string
:param epilog: the epilogue string
:param version: an optional version string
:returns: the argument parser
>>> ap = make_argparser(__file__, "This is a test program.",
... "This is a test.")
>>> isinstance(ap, ArgumentParser)
True
>>> from contextlib import redirect_stdout
>>> from io import StringIO
>>> s = StringIO()
>>> with redirect_stdout(s):
... ap.print_usage()
>>> print(s.getvalue())
usage: python3 -m pycommons.io.arguments [-h]
<BLANKLINE>
>>> s = StringIO()
>>> with redirect_stdout(s):
... ap.print_help()
>>> print(s.getvalue())
usage: python3 -m pycommons.io.arguments [-h]
<BLANKLINE>
This is a test program.
<BLANKLINE>
options:
-h, --help show this help message and exit
<BLANKLINE>
This is a test.
<BLANKLINE>
>>> ap = make_argparser(__file__, "This is a test program.",
... "This is a test.", "0.2")
>>> isinstance(ap, ArgumentParser)
True
>>> from contextlib import redirect_stdout
>>> from io import StringIO
>>> s = StringIO()
>>> with redirect_stdout(s):
... ap.print_usage()
>>> print(s.getvalue())
usage: python3 -m pycommons.io.arguments [-h] [--version]
<BLANKLINE>
>>> s = StringIO()
>>> with redirect_stdout(s):
... ap.print_help()
>>> print(s.getvalue())
usage: python3 -m pycommons.io.arguments [-h] [--version]
<BLANKLINE>
This is a test program.
<BLANKLINE>
options:
-h, --help show this help message and exit
--version show program's version number and exit
<BLANKLINE>
This is a test.
<BLANKLINE>
>>> ap = make_argparser(__file__, "This is a test program.",
... make_epilog("This program computes something",
... 2022, 2023, "Thomas Weise",
... url="https://github.com/thomasWeise/pycommons",
... email="tweise@hfuu.edu.cn"))
>>> s = StringIO()
>>> with redirect_stdout(s):
... ap.print_help()
>>> v = ('usage: python3 -m pycommons.io.arguments [-h]\n\nThis is '
... 'a test program.\n\noptions:\n -h, --help show this help '
... 'message and exit\n\nThis program computes something Copyright'
... '\xa0©\xa02022\u20112023,\xa0Thomas\xa0Weise,\nGNU\xa0GENERAL'
... '\xa0PUBLIC\xa0LICENSE\xa0Version\xa03,\xa029\xa0June'
... '\xa02007,\nhttps://github.com/thomasWeise/pycommons, '
... 'tweise@hfuu.edu.cn\n')
>>> s.getvalue() == v
True
>>> try:
... make_argparser(1, "", "")
... except TypeError as te:
... print(te)
descriptor '__len__' requires a 'str' object but received a 'int'
>>> try:
... make_argparser(None, "", "")
... except TypeError as te:
... print(te)
descriptor '__len__' requires a 'str' object but received a 'NoneType'
>>> try:
... make_argparser("te", "", "")
... except ValueError as ve:
... print(ve)
invalid file='te'.
>>> try:
... make_argparser("test", 1, "")
... except TypeError as te:
... print(te)
descriptor 'strip' for 'str' objects doesn't apply to a 'int' object
>>> try:
... make_argparser("Test", None, "")
... except TypeError as te:
... print(te)
descriptor 'strip' for 'str' objects doesn't apply to a 'NoneType' object
>>> try:
... make_argparser("Test", "Bla", "")
... except ValueError as ve:
... print(ve)
invalid description='Bla'.
>>> try:
... make_argparser("Test", "This is a long test", 1)
... except TypeError as te:
... print(te)
descriptor 'strip' for 'str' objects doesn't apply to a 'int' object
>>> try:
... make_argparser("Test", "This is a long test", None)
... except TypeError as te:
... print(te)
descriptor 'strip' for 'str' objects doesn't apply to a 'NoneType' object
>>> try:
... make_argparser("Test", "This is a long test", "epi")
... except ValueError as ve:
... print(ve)
invalid epilog='epi'.
>>> try:
... make_argparser(__file__, "This is a long test",
... "long long long epilog", 1)
... except TypeError as te:
... print(str(te)[:60])
descriptor 'strip' for 'str' objects doesn't apply to a 'int
>>> try:
... make_argparser(__file__, "This is a long test",
... "long long long epilog", " ")
... except ValueError as ve:
... print(ve)
Invalid version string ' '.
"""
if str.__len__(file) <= 3:
raise ValueError(f"invalid file={file!r}.")
description = str.strip(description)
if str.__len__(description) <= 12:
raise ValueError(f"invalid description={description!r}.")
epilog = str.strip(epilog)
if str.__len__(epilog) <= 10:
raise ValueError(f"invalid epilog={epilog!r}.")
result: Final[ArgumentParser] = ArgumentParser(
parents=[__DEFAULT_ARGUMENTS], prog=" ".join(python_command(file)),
description=description, epilog=epilog,
formatter_class=__DEFAULT_ARGUMENTS.formatter_class)
if version is not None:
uversion = str.strip(version)
if str.__len__(uversion) < 1:
raise ValueError(f"Invalid version string {version!r}.")
result.add_argument("--version", action="version", version=uversion)
return result
[docs]
def make_epilog(
text: str, copyright_start: int | None = None,
copyright_end: int | None = None, author: str | None = None,
the_license: str | None =
"GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007",
url: str | None = None,
email: str | None = None) -> str:
r"""
Build an epilogue from the given components.
:param text: the epilog text
:param copyright_start: the start year of the copyright, or `None` for no
copyright duration
:param copyright_end: the end year of the copyright, or `None` for using
the current year (unless `copyright_start` is `None`, in which case,
no copyright information is generated).
:param author: the author name, or `None` for no author
:param the_license: the license, or `None` for no license
:param url: the URL, or `None` for no URL
:param email: the email address(es) of the author, or `None` for no email
address information
:return: the copyright information
>>> cy = datetime.now(tz=timezone.utc).year
>>> ex = (f"This is a test.\n\nGNU\xa0GENERAL\xa0PUBLIC\xa0LICENSE"
... "\xa0Version\xa03,\xa029\xa0June\xa02007")
>>> make_epilog("This is a test.") == ex
True
>>> make_epilog("This is a test.", 2011, 2030, "Test User",
... "Test License", "http://testurl", "test@test.com")[:50]
'This is a test.\n\nCopyright\xa0©\xa02011\u20112030,\xa0Test\xa0User,'
>>> ex = (f"This is a test.\n\nCopyright\xa0©\xa02011\u2011{cy},"
... "\xa0Test\xa0User, Test\xa0License, http://testurl, "
... "test@test.com")
>>> make_epilog("This is a test.", 2011, None, "Test User",
... "Test License", "http://testurl", "test@test.com") == ex
True
>>> make_epilog("This is a test.", 2011, 2030, "Test User",
... "Test License", "http://testurl", "test@test.com")[50:]
' Test\xa0License, http://testurl, test@test.com'
>>> make_epilog("This is a test.", 2030, 2030, "Test User",
... "Test License", "http://testurl", "test@test.com")[:50]
'This is a test.\n\nCopyright\xa0©\xa02030,\xa0Test\xa0User, Test'
>>> make_epilog("This is a test.", 2030, 2030, "Test User",
... "Test License", "http://testurl", "test@test.com")[50:]
'\xa0License, http://testurl, test@test.com'
>>> make_epilog("This is a test.", None, None, "Test User",
... "Test License", "http://testurl", "test@test.com")[:50]
'This is a test.\n\nTest\xa0User, Test\xa0License, http://t'
>>> make_epilog("This is a test.", None, None, "Test User",
... "Test License", "http://testurl", "test@test.com")[50:]
'esturl, test@test.com'
>>> try:
... make_epilog(1, None, None, "Test User",
... "Test License", "http://testurl", "test@test.com")
... except TypeError as te:
... print(te)
descriptor 'strip' for 'str' objects doesn't apply to a 'int' object
>>> try:
... make_epilog(None, None, None, "Test User",
... "Test License", "http://testurl", "test@test.com")
... except TypeError as te:
... print(te)
descriptor 'strip' for 'str' objects doesn't apply to a 'NoneType' object
>>> try:
... make_epilog("1", None, None, "Test User",
... "Test License", "http://testurl", "test@test.com")
... except ValueError as ve:
... print(ve)
Epilog text too short: '1'.
>>> try:
... make_epilog("This is a test.", "v", None, "Test User",
... "Test License", "http://testurl", "test@test.com")
... except TypeError as te:
... print(te)
copyright_start should be an instance of int but is str, namely 'v'.
>>> try:
... make_epilog("This is a test.", -2, None, "Test User",
... "Test License", "http://testurl", "test@test.com")
... except ValueError as ve:
... print(ve)
copyright_start=-2 is invalid, must be in 1970..2500.
>>> try:
... make_epilog("This is a test.", 3455334, None, "Test User",
... "Test License", "http://testurl", "test@test.com")
... except ValueError as ve:
... print(ve)
copyright_start=3455334 is invalid, must be in 1970..2500.
>>> try:
... make_epilog("This is a test.", 2002, "v", "Test User",
... "Test License", "http://testurl", "test@test.com")
... except TypeError as te:
... print(te)
copyright_end should be an instance of int but is str, namely 'v'.
>>> try:
... make_epilog("This is a test.", 2002, 12, "Test User",
... "Test License", "http://testurl", "test@test.com")
... except ValueError as ve:
... print(ve)
copyright_end=12 is invalid, must be in 2002..2500.
>>> try:
... make_epilog("This is a test.", 2023, 3455334, "Test User",
... "Test License", "http://testurl", "test@test.com")
... except ValueError as ve:
... print(ve)
copyright_end=3455334 is invalid, must be in 2023..2500.
>>> try:
... make_epilog("This is a test.", None, None, 2,
... "Test License", "http://testurl", "test@test.com")
... except TypeError as te:
... print(te)
descriptor 'strip' for 'str' objects doesn't apply to a 'int' object
>>> try:
... make_epilog("This is a test.", None, None, "",
... "Test License", "http://testurl", "test@test.com")
... except ValueError as ve:
... print(ve)
Author too short: ''.
>>> try:
... make_epilog("This is a test.", None, None, "Tester",
... 23, "http://testurl", "test@test.com")
... except TypeError as te:
... print(te)
descriptor 'strip' for 'str' objects doesn't apply to a 'int' object
>>> try:
... make_epilog("This is a test.", None, None, "Tester",
... "Te", "http://testurl", "test@test.com")
... except ValueError as ve:
... print(ve)
License too short: 'Te'.
>>> try:
... make_epilog("This is a test.", None, None, "Tester",
... "GPL", 2, "test@test.com")
... except TypeError as te:
... print(te)
descriptor 'strip' for 'str' objects doesn't apply to a 'int' object
>>> try:
... make_epilog("This is a test.", None, None, "Tester",
... "GPL", "http", "test@test.com")
... except ValueError as ve:
... print(ve)
Url too short: 'http'.
>>> try:
... make_epilog("This is a test.", None, None, "Tester",
... "GPL", "http://www.test.com", 1)
... except TypeError as te:
... print(te)
descriptor 'strip' for 'str' objects doesn't apply to a 'int' object
>>> try:
... make_epilog("This is a test.", None, None, "Tester",
... "GPL", "http://www.test.com", "a@b")
... except ValueError as ve:
... print(ve)
Email too short: 'a@b'.
"""
text = str.strip(text)
if str.__len__(text) <= 10:
raise ValueError(f"Epilog text too short: {text!r}.")
the_epilog: str = ""
if copyright_start is not None:
copyright_start = check_int_range(
copyright_start, "copyright_start", 1970, 2500)
if copyright_end is None:
copyright_end = check_int_range(
datetime.now(tz=timezone.utc).year, "year", 1970, 2500)
else:
copyright_end = check_int_range(
copyright_end, "copyright_end", copyright_start, 2500)
the_epilog = f"Copyright \u00a9 {copyright_start}" \
if copyright_start >= copyright_end \
else f"Copyright \u00a9 {copyright_start}-{copyright_end}"
if author is not None:
author = str.strip(author)
if str.__len__(author) < 1:
raise ValueError(f"Author too short: {author!r}.")
the_epilog = f"{the_epilog}, {author}" \
if str.__len__(the_epilog) > 0 else author
if the_license is not None:
the_license = str.strip(the_license)
if str.__len__(the_license) < 3:
raise ValueError(f"License too short: {the_license!r}.")
the_epilog = f"{the_epilog},\n{the_license}" \
if str.__len__(the_epilog) > 0 else the_license
if url is not None:
url = str.strip(url)
if str.__len__(url) < 6:
raise ValueError(f"Url too short: {url!r}.")
the_epilog = f"{the_epilog},\n{url}" \
if str.__len__(the_epilog) > 0 else url
if email is not None:
email = str.strip(email)
if str.__len__(email) < 5:
raise ValueError(f"Email too short: {email!r}.")
the_epilog = f"{the_epilog},\n{email}" \
if str.__len__(the_epilog) > 0 else email
the_epilog = (the_epilog.replace(" ", NBSP)
.replace("-", NBDASH).replace("\n", " "))
return f"{text}\n\n{the_epilog}"
[docs]
def pycommons_argparser(
file: str, description: str, epilog: str) -> ArgumentParser:
"""
Create an argument parser with default settings for `pycommons`.
:param file: the `__file__` special variable of the calling script
:param description: the description string
:param epilog: the epilogue string
:returns: the argument parser
>>> ap = pycommons_argparser(
... __file__, "This is a test program.", "This is a test.")
>>> isinstance(ap, ArgumentParser)
True
>>> "Copyright" in ap.epilog
True
"""
return make_argparser(
file, description,
make_epilog(epilog, 2023, 2024, "Thomas Weise",
url="https://thomasweise.github.io/pycommons",
email="tweise@hfuu.edu.cn, tweise@ustc.edu.cn"),
__version__)