#!/usr/bin/python3
-# © Copyright 2021-2022, Scott Gasch
+# © Copyright 2021-2023, Scott Gasch
"""These are helpers for commandline argument parsing meant to work
-with Python's :mod:`argparse` module from the standard library. It
-contains validators for new argument types (such as free-form dates,
-durations, IP addresses, etc...) and an action that creates a pair of
-flags: one to disable a feature and another to enable it.
+with Python's :mod:`argparse` module from the standard library (See:
+https://docs.python.org/3/library/argparse.html). It contains
+validators for new argument types (such as free-form dates, durations,
+IP addresses, etc...) and an action that creates a pair of flags: one
+to disable a feature and another to enable it.
+
+See also :py:class:`pyutils.config.OptionalRawFormatter` which is
+automatically enabled if you use :py:mod:`config` module.
+
"""
import argparse
import datetime
import logging
import os
-from typing import Any
+from typing import Any, Optional
from overrides import overrides
These arguments can be used to indicate the inclusion or exclusion of
binary exclusive behaviors.
+
+ Raises:
+ ValueError: illegal argument value or combination
+
"""
- def __init__(self, option_strings, dest, default=None, required=False, help=None):
+ def __init__(
+ self,
+ option_strings: str,
+ dest: str,
+ default: Optional[str] = None,
+ required: bool = False,
+ help: Optional[str] = None,
+ ):
if default is None:
- msg = 'You must provide a default with Yes/No action'
+ msg = "You must provide a default with Yes/No action"
logger.critical(msg)
raise ValueError(msg)
if len(option_strings) != 1:
- msg = 'Only single argument is allowed with NoYes action'
+ msg = "Only single argument is allowed with NoYes action"
logger.critical(msg)
raise ValueError(msg)
opt = option_strings[0]
- if not opt.startswith('--'):
- msg = 'Yes/No arguments must be prefixed with --'
+ if not opt.startswith("--"):
+ msg = "Yes/No arguments must be prefixed with --"
logger.critical(msg)
raise ValueError(msg)
opt = opt[2:]
- opts = ['--' + opt, '--no_' + opt]
+ opts = ["--" + opt, "--no_" + opt]
super().__init__(
opts,
dest,
)
@overrides
- def __call__(self, parser, namespace, values, option_strings=None):
- if option_strings.startswith('--no-') or option_strings.startswith('--no_'):
- setattr(namespace, self.dest, False)
- else:
- setattr(namespace, self.dest, True)
+ def __call__(self, parser, namespace, values, option_strings: Optional[str] = None):
+ if option_strings is not None:
+ if option_strings.startswith("--no-") or option_strings.startswith("--no_"):
+ setattr(namespace, self.dest, False)
+ else:
+ setattr(namespace, self.dest, True)
def valid_bool(v: Any) -> bool:
v: data passed to an argument expecting a bool on the cmdline.
Returns:
- The boolean value of v or raises an ArgumentTypeError on error.
+ The boolean value of v
+
+ Raises:
+ ArgumentTypeError: parse error (e.g. not a valid bool string)
Sample usage::
ip: data passed to a commandline arg expecting an IP(v4) address.
Returns:
- The IP address, if valid. Raises ArgumentTypeError otherwise.
+ The IP address, if valid.
+
+ Raises:
+ ArgumentTypeError: parse error (e.g. not a valid IP address string)
Sample usage::
mac: a value passed to a commandline flag expecting a MAC address.
Returns:
- The MAC address passed or raises ArgumentTypeError on error.
+ The MAC address passed
+
+ Raises:
+ ArgumentTypeError: parse error (e.g. not a valid MAC address)
Sample usage::
Returns:
The number if valid, otherwise raises ArgumentTypeError.
+ Raises:
+ ArgumentTypeError: parse error (e.g. not a valid percentage)
+
Sample usage::
args.add_argument(
argparse.ArgumentTypeError: 115 is an invalid percentage; expected 0 <= n <= 100.0
"""
- num = num.strip('%')
+ num = num.strip("%")
n = float(num)
if 0.0 <= n <= 100.0:
return n
Returns:
The filename if valid, otherwise raises ArgumentTypeError.
+ Raises:
+ ArgumentTypeError: parse error (e.g. file doesn't exist)
+
Sample usage::
args.add_argument(
txt: data passed to a commandline flag expecting a date.
Returns:
- the datetime.date described by txt or raises ArgumentTypeError on error.
+ the datetime.date described by txt
+
+ Raises:
+ ArgumentTypeError: parse error (e.g. date not valid)
Sample usage::
.. note::
dates like 'next wednesday' work fine, they are just
hard to doctest for without knowing when the testcase will be
- executed...
+ executed... See :py:mod:`pyutils.datetimes.dateparse_utils`
+ for other examples of usable expressions.
>>> valid_date('next wednesday') # doctest: +ELLIPSIS
-ANYTHING-
date = to_date(txt)
if date is not None:
return date
- msg = f'Cannot parse argument as a date: {txt}'
+ msg = f"Cannot parse argument as a date: {txt}"
logger.error(msg)
raise argparse.ArgumentTypeError(msg)
txt: data passed to a commandline flag expecting a valid datetime.datetime.
Returns:
- The datetime.datetime described by txt or raises ArgumentTypeError on error.
+ The datetime.datetime described by txt
+
+ Raises:
+ ArgumentTypeError: parse error (e.g. invalid datetime string)
Sample usage::
>>> valid_datetime('6/5/2021 3:01:02')
datetime.datetime(2021, 6, 5, 3, 1, 2)
+ >>> valid_datetime('Sun Dec 11 11:50:00 PST 2022')
+ datetime.datetime(2022, 12, 11, 11, 50)
+
.. note::
Because this code uses an English date-expression parsing grammar
internally, much more complex datetimes can be expressed in free form.
- See: `tests/datetimez/dateparse_utils_test.py` for examples. These
+ See :mod:`pyutils.datetimes.dateparse_utils` for details. These
are not included in here because they are hard to write valid doctests
for!
dt = to_datetime(txt)
if dt is not None:
return dt
- msg = f'Cannot parse argument as datetime: {txt}'
+
+ # Don't choke on the default format of unix date.
+ try:
+ return datetime.datetime.strptime(txt, "%a %b %d %H:%M:%S %Z %Y")
+ except Exception:
+ pass
+
+ msg = f"Cannot parse argument as datetime: {txt}"
logger.error(msg)
raise argparse.ArgumentTypeError(msg)
txt: data passed to a commandline arg expecting a duration.
Returns:
- The datetime.timedelta described by txt or raises ArgumentTypeError
- on error.
+ The datetime.timedelta described by txt.
+
+ Raises:
+ ArgumentTypeError: parse error (e.g. invalid duration string)
Sample usage::
...
argparse.ArgumentTypeError: a little while is not a valid duration.
"""
- from pyutils.datetimez.datetime_utils import parse_duration
+ from pyutils.datetimes.datetime_utils import parse_duration
try:
secs = parse_duration(txt, raise_on_error=True)
return datetime.timedelta(seconds=secs)
except Exception as e:
- logger.exception(e)
+ logger.exception("Exception while parsing a supposed duration: %s", txt)
+ raise argparse.ArgumentTypeError(e) from e
+
+
+def valid_byte_count(txt: str) -> int:
+ """If the string is a valid number of bytes, return an integer
+ representing the requested byte count. This method uses
+ :meth:`string_utils.suffix_string_to_number` to parse and and
+ accepts / understands:
+
+ - plain numbers (123456)
+ - numbers with ISO suffixes (Mb, Gb, Pb, etc...)
+
+ Args:
+ txt: data passed to a commandline arg expecting a duration.
+
+ Returns:
+ An integer number of bytes.
+
+ Raises:
+ ArgumentTypeError: parse error (e.g. byte count not parsable)
+
+ Sample usage::
+
+ cfg.add_argument(
+ '--max_file_size',
+ type=argparse_utils.valid_byte_count,
+ default=(1024 * 1024),
+ metavar='NUM_BYTES',
+ help='The largest file we may create',
+ )
+
+ >>> valid_byte_count('1Mb')
+ 1048576
+
+ >>> valid_byte_count("1234567")
+ 1234567
+
+ >>> valid_byte_count("1M")
+ 1048576
+
+ >>> valid_byte_count("1.2Gb")
+ 1288490188
+
+ >>> valid_byte_count('1.2') # <--- contains a decimal
+ Traceback (most recent call last):
+ ...
+ argparse.ArgumentTypeError: Invalid byte count: 1.2
+
+ >>> valid_byte_count(1234567) # <--- not a string
+ Traceback (most recent call last):
+ ...
+ argparse.ArgumentTypeError: Invalid byte count: 1234567
+
+ >>> valid_byte_count('On a dark and stormy night')
+ Traceback (most recent call last):
+ ...
+ argparse.ArgumentTypeError: Invalid byte count: On a dark and stormy night
+
+ """
+ from pyutils.string_utils import suffix_string_to_number
+
+ try:
+ num_bytes = suffix_string_to_number(txt)
+ if num_bytes:
+ return num_bytes
+ raise argparse.ArgumentTypeError(f"Invalid byte count: {txt}")
+ except Exception as e:
+ logger.exception("Exception while parsing a supposed byte count: %s", txt)
raise argparse.ArgumentTypeError(e) from e
-if __name__ == '__main__':
+if __name__ == "__main__":
import doctest
- doctest.ELLIPSIS_MARKER = '-ANYTHING-'
+ doctest.ELLIPSIS_MARKER = "-ANYTHING-"
doctest.testmod()