X-Git-Url: https://wannabe.guru.org/gitweb/?a=blobdiff_plain;f=src%2Fpyutils%2Fargparse_utils.py;h=a0b4cc24d587690d9a59564e8000a656c2c7c645;hb=278d163705facc2276cd464414fb490ef6af50ab;hp=3b466b0d93da43bcf69c98215fc993bb121871e6;hpb=69566c003b4f1c3a4905f37d3735d7921502d14a;p=pyutils.git diff --git a/src/pyutils/argparse_utils.py b/src/pyutils/argparse_utils.py index 3b466b0..a0b4cc2 100644 --- a/src/pyutils/argparse_utils.py +++ b/src/pyutils/argparse_utils.py @@ -2,7 +2,17 @@ # © Copyright 2021-2022, Scott Gasch -"""Helpers for commandline argument parsing.""" +"""These are helpers for commandline argument parsing meant to work +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 @@ -74,7 +84,23 @@ class ActionNoYes(argparse.Action): def valid_bool(v: Any) -> bool: """ - If the string is a valid bool, return its value. + If the string is a valid bool, return its value. Otherwise raise. + + Args: + v: data passed to an argument expecting a bool on the cmdline. + + Returns: + The boolean value of v or raises an ArgumentTypeError on error. + + Sample usage:: + + args.add_argument( + '--auto', + type=argparse_utils.valid_bool, + default=None, + metavar='True|False', + help='Use your best judgement about --primary and --secondary', + ) >>> valid_bool(True) True @@ -91,6 +117,9 @@ def valid_bool(v: Any) -> bool: >>> valid_bool("1") True + >>> valid_bool("off") # Note: expect False; invalid would raise. + False + >>> valid_bool(12345) Traceback (most recent call last): ... @@ -112,6 +141,22 @@ def valid_ip(ip: str) -> str: If the string is a valid IPv4 address, return it. Otherwise raise an ArgumentTypeError. + Args: + ip: data passed to a commandline arg expecting an IP(v4) address. + + Returns: + The IP address, if valid. Raises ArgumentTypeError otherwise. + + Sample usage:: + + args.add_argument( + "-i", + "--ip_address", + metavar="TARGET_IP_ADDRESS", + help="Target IP Address", + type=argparse_utils.valid_ip, + ) + >>> valid_ip("1.2.3.4") '1.2.3.4' @@ -136,6 +181,22 @@ def valid_mac(mac: str) -> str: If the string is a valid MAC address, return it. Otherwise raise an ArgumentTypeError. + Args: + mac: a value passed to a commandline flag expecting a MAC address. + + Returns: + The MAC address passed or raises ArgumentTypeError on error. + + Sample usage:: + + group.add_argument( + "-m", + "--mac", + metavar="MAC_ADDRESS", + help="Target MAC Address", + type=argparse_utils.valid_mac, + ) + >>> valid_mac('12:23:3A:4F:55:66') '12:23:3A:4F:55:66' @@ -160,8 +221,24 @@ def valid_mac(mac: str) -> str: def valid_percentage(num: str) -> float: """ - If the string is a valid percentage, return it. Otherwise raise - an ArgumentTypeError. + If the string is a valid (0 <= n <= 100) percentage, return it. + Otherwise raise an ArgumentTypeError. + + Arg: + num: data passed to a flag expecting a percentage with a value + between 0 and 100 inclusive. + + Returns: + The number if valid, otherwise raises ArgumentTypeError. + + Sample usage:: + + args.add_argument( + '--percent_change', + type=argparse_utils.valid_percentage, + default=0, + help='The percent change (0<=n<=100) of foobar', + ) >>> valid_percentage("15%") 15.0 @@ -186,8 +263,29 @@ def valid_percentage(num: str) -> float: def valid_filename(filename: str) -> str: """ - If the string is a valid filename, return it. Otherwise raise - an ArgumentTypeError. + If the string contains a valid filename that exists on the filesystem, + return it. Otherwise raise an ArgumentTypeError. + + .. note:: + + This method will accept directories that exist on the filesystem + in addition to files. + + Args: + filename: data passed to a flag expecting a valid filename. + + Returns: + The filename if valid, otherwise raises ArgumentTypeError. + + Sample usage:: + + args.add_argument( + '--network_mac_addresses_file', + default='/home/scott/bin/network_mac_addresses.txt', + metavar='FILENAME', + help='Location of the network_mac_addresses file (must exist!).', + type=argparse_utils.valid_filename, + ) >>> valid_filename('/tmp') '/tmp' @@ -210,12 +308,30 @@ def valid_date(txt: str) -> datetime.date: """If the string is a valid date, return it. Otherwise raise an ArgumentTypeError. + Args: + txt: data passed to a commandline flag expecting a date. + + Returns: + the datetime.date described by txt or raises ArgumentTypeError on error. + + Sample usage:: + + cfg.add_argument( + "--date", + nargs=1, + type=argparse_utils.valid_date, + metavar="DATE STRING", + default=None + ) + >>> valid_date('6/5/2021') datetime.date(2021, 6, 5) - # Note: dates like 'next wednesday' work fine, they are just - # hard to test for without knowing when the testcase will be - # executed... + .. note:: + dates like 'next wednesday' work fine, they are just + hard to doctest for without knowing when the testcase will be + executed... + >>> valid_date('next wednesday') # doctest: +ELLIPSIS -ANYTHING- """ @@ -233,12 +349,33 @@ def valid_datetime(txt: str) -> datetime.datetime: """If the string is a valid datetime, return it. Otherwise raise an ArgumentTypeError. + Args: + txt: data passed to a commandline flag expecting a valid datetime.datetime. + + Returns: + The datetime.datetime described by txt or raises ArgumentTypeError on error. + + Sample usage:: + + cfg.add_argument( + "--override_timestamp", + nargs=1, + type=argparse_utils.valid_datetime, + help="Don't use the current datetime, override to argument.", + metavar="DATE/TIME STRING", + default=None, + ) + >>> valid_datetime('6/5/2021 3:01:02') datetime.datetime(2021, 6, 5, 3, 1, 2) - # Again, these types of expressions work fine but are - # difficult to test with doctests because the answer is - # relative to the time the doctest is executed. + .. note:: + Because this code uses an English date-expression parsing grammar + internally, much more complex datetimes can be expressed in free form. + See :mod:`pyutils.datetimez.dateparse_utils` for details. These + are not included in here because they are hard to write valid doctests + for! + >>> valid_datetime('next christmas at 4:15am') # doctest: +ELLIPSIS -ANYTHING- """ @@ -254,21 +391,57 @@ def valid_datetime(txt: str) -> datetime.datetime: def valid_duration(txt: str) -> datetime.timedelta: """If the string is a valid time duration, return a - datetime.timedelta representing the period of time. Otherwise - maybe raise an ArgumentTypeError or potentially just treat the - time window as zero in length. + datetime.timedelta representing the duration described. + This uses `datetime_utils.parse_duration` to parse durations + and expects data such as: + + - 15 days, 3 hours, 15 minutes + - 15 days 3 hours 15 minutes + - 15d 3h 15m + - 15d3h5m + - 3m 2s + - 1000s + + If the duration is not parsable, raise an ArgumentTypeError. + + Args: + txt: data passed to a commandline arg expecting a duration. + + Returns: + The datetime.timedelta described by txt or raises ArgumentTypeError + on error. + + Sample usage:: + + cfg.add_argument( + '--ip_cache_max_staleness', + type=argparse_utils.valid_duration, + default=datetime.timedelta(seconds=60 * 60 * 4), + metavar='DURATION', + help='Max acceptable age of the IP address cache' + ) + + >>> valid_duration('15d3h5m') + datetime.timedelta(days=15, seconds=11100) + + >>> valid_duration('15 days 3 hours 5 min') + datetime.timedelta(days=15, seconds=11100) >>> valid_duration('3m') datetime.timedelta(seconds=180) - >>> valid_duration('your mom') - datetime.timedelta(0) + >>> valid_duration('3 days, 2 hours') + datetime.timedelta(days=3, seconds=7200) + >>> valid_duration('a little while') + Traceback (most recent call last): + ... + argparse.ArgumentTypeError: a little while is not a valid duration. """ from pyutils.datetimez.datetime_utils import parse_duration try: - secs = parse_duration(txt) + secs = parse_duration(txt, raise_on_error=True) return datetime.timedelta(seconds=secs) except Exception as e: logger.exception(e)