3 """Helpers for commandline argument parsing."""
11 from overrides import overrides
13 # This module is commonly used by others in here and should avoid
14 # taking any unnecessary dependencies back on them.
16 logger = logging.getLogger(__name__)
19 class ActionNoYes(argparse.Action):
20 """An argparse Action that allows for commandline arguments like this:
26 help='Should we enable the thing?'
29 This creates cmdline arguments:
35 def __init__(self, option_strings, dest, default=None, required=False, help=None):
37 msg = 'You must provide a default with Yes/No action'
40 if len(option_strings) != 1:
41 msg = 'Only single argument is allowed with NoYes action'
44 opt = option_strings[0]
45 if not opt.startswith('--'):
46 msg = 'Yes/No arguments must be prefixed with --'
51 opts = ['--' + opt, '--no_' + opt]
63 def __call__(self, parser, namespace, values, option_strings=None):
64 if option_strings.startswith('--no-') or option_strings.startswith('--no_'):
65 setattr(namespace, self.dest, False)
67 setattr(namespace, self.dest, True)
70 def valid_bool(v: Any) -> bool:
72 If the string is a valid bool, return its value.
77 >>> valid_bool("true")
90 Traceback (most recent call last):
92 argparse.ArgumentTypeError: 12345
95 if isinstance(v, bool):
97 from string_utils import to_bool
101 except Exception as e:
102 raise argparse.ArgumentTypeError(v) from e
105 def valid_ip(ip: str) -> str:
107 If the string is a valid IPv4 address, return it. Otherwise raise
108 an ArgumentTypeError.
110 >>> valid_ip("1.2.3.4")
113 >>> valid_ip("localhost")
114 Traceback (most recent call last):
116 argparse.ArgumentTypeError: localhost is an invalid IP address
119 from string_utils import extract_ip_v4
121 s = extract_ip_v4(ip.strip())
124 msg = f"{ip} is an invalid IP address"
126 raise argparse.ArgumentTypeError(msg)
129 def valid_mac(mac: str) -> str:
131 If the string is a valid MAC address, return it. Otherwise raise
132 an ArgumentTypeError.
134 >>> valid_mac('12:23:3A:4F:55:66')
137 >>> valid_mac('12-23-3A-4F-55-66')
141 Traceback (most recent call last):
143 argparse.ArgumentTypeError: big is an invalid MAC address
146 from string_utils import extract_mac_address
148 s = extract_mac_address(mac)
151 msg = f"{mac} is an invalid MAC address"
153 raise argparse.ArgumentTypeError(msg)
156 def valid_percentage(num: str) -> float:
158 If the string is a valid percentage, return it. Otherwise raise
159 an ArgumentTypeError.
161 >>> valid_percentage("15%")
164 >>> valid_percentage('40')
167 >>> valid_percentage('115')
168 Traceback (most recent call last):
170 argparse.ArgumentTypeError: 115 is an invalid percentage; expected 0 <= n <= 100.0
175 if 0.0 <= n <= 100.0:
177 msg = f"{num} is an invalid percentage; expected 0 <= n <= 100.0"
179 raise argparse.ArgumentTypeError(msg)
182 def valid_filename(filename: str) -> str:
184 If the string is a valid filename, return it. Otherwise raise
185 an ArgumentTypeError.
187 >>> valid_filename('/tmp')
190 >>> valid_filename('wfwefwefwefwefwefwefwefwef')
191 Traceback (most recent call last):
193 argparse.ArgumentTypeError: wfwefwefwefwefwefwefwefwef was not found and is therefore invalid.
197 if os.path.exists(s):
199 msg = f"{filename} was not found and is therefore invalid."
201 raise argparse.ArgumentTypeError(msg)
204 def valid_date(txt: str) -> datetime.date:
205 """If the string is a valid date, return it. Otherwise raise
206 an ArgumentTypeError.
208 >>> valid_date('6/5/2021')
209 datetime.date(2021, 6, 5)
211 # Note: dates like 'next wednesday' work fine, they are just
212 # hard to test for without knowing when the testcase will be
214 >>> valid_date('next wednesday') # doctest: +ELLIPSIS
217 from string_utils import to_date
222 msg = f'Cannot parse argument as a date: {txt}'
224 raise argparse.ArgumentTypeError(msg)
227 def valid_datetime(txt: str) -> datetime.datetime:
228 """If the string is a valid datetime, return it. Otherwise raise
229 an ArgumentTypeError.
231 >>> valid_datetime('6/5/2021 3:01:02')
232 datetime.datetime(2021, 6, 5, 3, 1, 2)
234 # Again, these types of expressions work fine but are
235 # difficult to test with doctests because the answer is
236 # relative to the time the doctest is executed.
237 >>> valid_datetime('next christmas at 4:15am') # doctest: +ELLIPSIS
240 from string_utils import to_datetime
242 dt = to_datetime(txt)
245 msg = f'Cannot parse argument as datetime: {txt}'
247 raise argparse.ArgumentTypeError(msg)
250 def valid_duration(txt: str) -> datetime.timedelta:
251 """If the string is a valid time duration, return a
252 datetime.timedelta representing the period of time. Otherwise
253 maybe raise an ArgumentTypeError or potentially just treat the
254 time window as zero in length.
256 >>> valid_duration('3m')
257 datetime.timedelta(seconds=180)
259 >>> valid_duration('your mom')
260 datetime.timedelta(0)
263 from datetime_utils import parse_duration
266 secs = parse_duration(txt)
267 return datetime.timedelta(seconds=secs)
268 except Exception as e:
270 raise argparse.ArgumentTypeError(e) from e
273 if __name__ == '__main__':
276 doctest.ELLIPSIS_MARKER = '-ANYTHING-'