3 # © Copyright 2021-2022, Scott Gasch
5 """Helpers for commandline argument parsing."""
11 from typing import Any
13 from overrides import overrides
15 # This module is commonly used by others in here and should avoid
16 # taking any unnecessary dependencies back on them.
18 logger = logging.getLogger(__name__)
21 class ActionNoYes(argparse.Action):
22 """An argparse Action that allows for commandline arguments like this:
28 help='Should we enable the thing?'
31 This creates cmdline arguments:
36 These arguments can be used to indicate the inclusion or exclusion of
37 binary exclusive behaviors.
40 def __init__(self, option_strings, dest, default=None, required=False, help=None):
42 msg = 'You must provide a default with Yes/No action'
45 if len(option_strings) != 1:
46 msg = 'Only single argument is allowed with NoYes action'
49 opt = option_strings[0]
50 if not opt.startswith('--'):
51 msg = 'Yes/No arguments must be prefixed with --'
56 opts = ['--' + opt, '--no_' + opt]
68 def __call__(self, parser, namespace, values, option_strings=None):
69 if option_strings.startswith('--no-') or option_strings.startswith('--no_'):
70 setattr(namespace, self.dest, False)
72 setattr(namespace, self.dest, True)
75 def valid_bool(v: Any) -> bool:
77 If the string is a valid bool, return its value.
82 >>> valid_bool("true")
95 Traceback (most recent call last):
97 argparse.ArgumentTypeError: 12345
100 if isinstance(v, bool):
102 from string_utils import to_bool
106 except Exception as e:
107 raise argparse.ArgumentTypeError(v) from e
110 def valid_ip(ip: str) -> str:
112 If the string is a valid IPv4 address, return it. Otherwise raise
113 an ArgumentTypeError.
115 >>> valid_ip("1.2.3.4")
118 >>> valid_ip("localhost")
119 Traceback (most recent call last):
121 argparse.ArgumentTypeError: localhost is an invalid IP address
124 from string_utils import extract_ip_v4
126 s = extract_ip_v4(ip.strip())
129 msg = f"{ip} is an invalid IP address"
131 raise argparse.ArgumentTypeError(msg)
134 def valid_mac(mac: str) -> str:
136 If the string is a valid MAC address, return it. Otherwise raise
137 an ArgumentTypeError.
139 >>> valid_mac('12:23:3A:4F:55:66')
142 >>> valid_mac('12-23-3A-4F-55-66')
146 Traceback (most recent call last):
148 argparse.ArgumentTypeError: big is an invalid MAC address
151 from string_utils import extract_mac_address
153 s = extract_mac_address(mac)
156 msg = f"{mac} is an invalid MAC address"
158 raise argparse.ArgumentTypeError(msg)
161 def valid_percentage(num: str) -> float:
163 If the string is a valid percentage, return it. Otherwise raise
164 an ArgumentTypeError.
166 >>> valid_percentage("15%")
169 >>> valid_percentage('40')
172 >>> valid_percentage('115')
173 Traceback (most recent call last):
175 argparse.ArgumentTypeError: 115 is an invalid percentage; expected 0 <= n <= 100.0
180 if 0.0 <= n <= 100.0:
182 msg = f"{num} is an invalid percentage; expected 0 <= n <= 100.0"
184 raise argparse.ArgumentTypeError(msg)
187 def valid_filename(filename: str) -> str:
189 If the string is a valid filename, return it. Otherwise raise
190 an ArgumentTypeError.
192 >>> valid_filename('/tmp')
195 >>> valid_filename('wfwefwefwefwefwefwefwefwef')
196 Traceback (most recent call last):
198 argparse.ArgumentTypeError: wfwefwefwefwefwefwefwefwef was not found and is therefore invalid.
202 if os.path.exists(s):
204 msg = f"{filename} was not found and is therefore invalid."
206 raise argparse.ArgumentTypeError(msg)
209 def valid_date(txt: str) -> datetime.date:
210 """If the string is a valid date, return it. Otherwise raise
211 an ArgumentTypeError.
213 >>> valid_date('6/5/2021')
214 datetime.date(2021, 6, 5)
216 # Note: dates like 'next wednesday' work fine, they are just
217 # hard to test for without knowing when the testcase will be
219 >>> valid_date('next wednesday') # doctest: +ELLIPSIS
222 from string_utils import to_date
227 msg = f'Cannot parse argument as a date: {txt}'
229 raise argparse.ArgumentTypeError(msg)
232 def valid_datetime(txt: str) -> datetime.datetime:
233 """If the string is a valid datetime, return it. Otherwise raise
234 an ArgumentTypeError.
236 >>> valid_datetime('6/5/2021 3:01:02')
237 datetime.datetime(2021, 6, 5, 3, 1, 2)
239 # Again, these types of expressions work fine but are
240 # difficult to test with doctests because the answer is
241 # relative to the time the doctest is executed.
242 >>> valid_datetime('next christmas at 4:15am') # doctest: +ELLIPSIS
245 from string_utils import to_datetime
247 dt = to_datetime(txt)
250 msg = f'Cannot parse argument as datetime: {txt}'
252 raise argparse.ArgumentTypeError(msg)
255 def valid_duration(txt: str) -> datetime.timedelta:
256 """If the string is a valid time duration, return a
257 datetime.timedelta representing the period of time. Otherwise
258 maybe raise an ArgumentTypeError or potentially just treat the
259 time window as zero in length.
261 >>> valid_duration('3m')
262 datetime.timedelta(seconds=180)
264 >>> valid_duration('your mom')
265 datetime.timedelta(0)
268 from datetime_utils import parse_duration
271 secs = parse_duration(txt)
272 return datetime.timedelta(seconds=secs)
273 except Exception as e:
275 raise argparse.ArgumentTypeError(e) from e
278 if __name__ == '__main__':
281 doctest.ELLIPSIS_MARKER = '-ANYTHING-'