Easier and more self documenting patterns for loading/saving Persistent
[python_utils.git] / argparse_utils.py
index 0ee2be9f8017093caeff732962ad9b4d10a993e7..f73a8936d3eb268b96ea387a543608d24d0ceb51 100644 (file)
@@ -1,11 +1,17 @@
 #!/usr/bin/python3
 
+# © Copyright 2021-2022, Scott Gasch
+
+"""Helpers for commandline argument parsing."""
+
 import argparse
 import datetime
 import logging
 import os
 from typing import Any
 
+from overrides import overrides
+
 # This module is commonly used by others in here and should avoid
 # taking any unnecessary dependencies back on them.
 
@@ -13,20 +19,31 @@ logger = logging.getLogger(__name__)
 
 
 class ActionNoYes(argparse.Action):
-    def __init__(
-            self,
-            option_strings,
-            dest,
-            default=None,
-            required=False,
-            help=None
-    ):
+    """An argparse Action that allows for commandline arguments like this::
+
+        cfg.add_argument(
+            '--enable_the_thing',
+            action=ActionNoYes,
+            default=False,
+            help='Should we enable the thing?'
+        )
+
+    This creates the following cmdline arguments::
+
+        --enable_the_thing
+        --no_enable_the_thing
+
+    These arguments can be used to indicate the inclusion or exclusion of
+    binary exclusive behaviors.
+    """
+
+    def __init__(self, option_strings, dest, default=None, required=False, help=None):
         if default is None:
             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 YesNo action'
+            msg = 'Only single argument is allowed with NoYes action'
             logger.critical(msg)
             raise ValueError(msg)
         opt = option_strings[0]
@@ -44,14 +61,12 @@ class ActionNoYes(argparse.Action):
             const=None,
             default=default,
             required=required,
-            help=help
+            help=help,
         )
 
+    @overrides
     def __call__(self, parser, namespace, values, option_strings=None):
-        if (
-                option_strings.startswith('--no-') or
-                option_strings.startswith('--no_')
-        ):
+        if option_strings.startswith('--no-') or option_strings.startswith('--no_'):
             setattr(namespace, self.dest, False)
         else:
             setattr(namespace, self.dest, True)
@@ -85,10 +100,11 @@ def valid_bool(v: Any) -> bool:
     if isinstance(v, bool):
         return v
     from string_utils import to_bool
+
     try:
         return to_bool(v)
-    except Exception:
-        raise argparse.ArgumentTypeError(v)
+    except Exception as e:
+        raise argparse.ArgumentTypeError(v) from e
 
 
 def valid_ip(ip: str) -> str:
@@ -106,11 +122,12 @@ def valid_ip(ip: str) -> str:
 
     """
     from string_utils import extract_ip_v4
+
     s = extract_ip_v4(ip.strip())
     if s is not None:
         return s
     msg = f"{ip} is an invalid IP address"
-    logger.warning(msg)
+    logger.error(msg)
     raise argparse.ArgumentTypeError(msg)
 
 
@@ -132,11 +149,12 @@ def valid_mac(mac: str) -> str:
 
     """
     from string_utils import extract_mac_address
+
     s = extract_mac_address(mac)
     if s is not None:
         return s
     msg = f"{mac} is an invalid MAC address"
-    logger.warning(msg)
+    logger.error(msg)
     raise argparse.ArgumentTypeError(msg)
 
 
@@ -162,7 +180,7 @@ def valid_percentage(num: str) -> float:
     if 0.0 <= n <= 100.0:
         return n
     msg = f"{num} is an invalid percentage; expected 0 <= n <= 100.0"
-    logger.warning(msg)
+    logger.error(msg)
     raise argparse.ArgumentTypeError(msg)
 
 
@@ -184,7 +202,7 @@ def valid_filename(filename: str) -> str:
     if os.path.exists(s):
         return s
     msg = f"{filename} was not found and is therefore invalid."
-    logger.warning(msg)
+    logger.error(msg)
     raise argparse.ArgumentTypeError(msg)
 
 
@@ -202,11 +220,12 @@ def valid_date(txt: str) -> datetime.date:
     -ANYTHING-
     """
     from string_utils import to_date
+
     date = to_date(txt)
     if date is not None:
         return date
     msg = f'Cannot parse argument as a date: {txt}'
-    logger.warning(msg)
+    logger.error(msg)
     raise argparse.ArgumentTypeError(msg)
 
 
@@ -224,15 +243,40 @@ def valid_datetime(txt: str) -> datetime.datetime:
     -ANYTHING-
     """
     from string_utils import to_datetime
+
     dt = to_datetime(txt)
     if dt is not None:
         return dt
     msg = f'Cannot parse argument as datetime: {txt}'
-    logger.warning(msg)
+    logger.error(msg)
     raise argparse.ArgumentTypeError(msg)
 
 
+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.
+
+    >>> valid_duration('3m')
+    datetime.timedelta(seconds=180)
+
+    >>> valid_duration('your mom')
+    datetime.timedelta(0)
+
+    """
+    from datetime_utils import parse_duration
+
+    try:
+        secs = parse_duration(txt)
+        return datetime.timedelta(seconds=secs)
+    except Exception as e:
+        logger.exception(e)
+        raise argparse.ArgumentTypeError(e) from e
+
+
 if __name__ == '__main__':
     import doctest
+
     doctest.ELLIPSIS_MARKER = '-ANYTHING-'
     doctest.testmod()