Skip to content

Commit ad0e71c

Browse files
committed
Introduce argparse based argument parsing with getopt compatibility.
Add a shared module intended to handler arguments parsing. Expose methods that wrap argparse accepting list of named arguments to treat as strings, booleans and integers respectively. This is purposefully designed as drop-in for the style used in the generateconfs cli. Also implement a getopt replacement. This parses getopt strings in order to build up known arguments then calls the previously defined shared module - in this way the new module is also a drop-in for any callsite using getopt (e.g. createuser). Support both the naming of arguments and help text. Additionally provide a function able to break up pre-existing usage strings used with relatively few changes can be re-used for new-style arguments.
1 parent 8b53f18 commit ad0e71c

File tree

2 files changed

+180
-0
lines changed

2 files changed

+180
-0
lines changed

mig/shared/minimist.py

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import argparse
2+
3+
4+
_EMPTY_DICT = {}
5+
_NO_DEFAULT = object()
6+
_POSITIONAL_MARKER = '__args__'
7+
8+
9+
class GetoptCompatNamespace(argparse.Namespace):
10+
def __iter__(self):
11+
return iter(("-%s" % (k,), v) for k, v in self.__dict__.items() if k != _POSITIONAL_MARKER)
12+
13+
14+
def _arg_name_to_flag(arg_name, is_getopt):
15+
if is_getopt:
16+
flag_prefix = "-"
17+
else:
18+
flag_prefix = "--"
19+
return ''.join((flag_prefix, arg_name))
20+
21+
22+
def _arg_name_to_default(arg_name, _defaults):
23+
try:
24+
return _defaults[arg_name]
25+
except KeyError:
26+
return None
27+
28+
29+
def minimist(prog, description, strings=[], booleans=[], integers=[],
30+
help_by_argument=_EMPTY_DICT, name_by_argument=_EMPTY_DICT,
31+
is_getopt=False, use_positional=False, _defaults=None):
32+
parser = argparse.ArgumentParser(prog, allow_abbrev=False)
33+
34+
if _defaults is None:
35+
_defaults = {}
36+
37+
for arg_name in booleans:
38+
arg_flag = _arg_name_to_flag(arg_name, is_getopt)
39+
parser.add_argument(arg_flag, action='store_true',
40+
help=help_by_argument.get(arg_name, None))
41+
42+
for arg_name in strings:
43+
if is_getopt:
44+
arg_fallback = argparse.SUPPRESS
45+
else:
46+
arg_fallback = None
47+
arg_default = _defaults.get(arg_name, arg_fallback)
48+
arg_flag = _arg_name_to_flag(arg_name, is_getopt)
49+
parser.add_argument(arg_flag, default=arg_default,
50+
metavar=name_by_argument.get(arg_name),
51+
help=help_by_argument.get(arg_name, None))
52+
53+
for arg_name in integers:
54+
arg_flag = _arg_name_to_flag(arg_name, is_getopt)
55+
parser.add_argument(arg_flag, type=int,
56+
metavar=name_by_argument.get(arg_name),
57+
help=help_by_argument.get(arg_name, None))
58+
59+
if is_getopt or use_positional:
60+
if is_getopt:
61+
arg_fallback = argparse.SUPPRESS
62+
else:
63+
arg_fallback = None
64+
parser.add_argument(_POSITIONAL_MARKER, nargs='*',
65+
default=arg_fallback)
66+
67+
return parser
68+
69+
70+
def _parse_getopt_string(getopt_string):
71+
split_args = getopt_string.split(':')
72+
73+
seen_string_args = set()
74+
75+
boolean_arguments = []
76+
string_arguments = []
77+
78+
# handle corner case of no arguments with value i.e. no separator in input
79+
# FIXME: ...
80+
81+
def add_string_argument(arg):
82+
if arg == 'h': # exclude -h which is handled internally by argparse
83+
return
84+
string_arguments.append(arg)
85+
86+
index_of_last_entry = len(split_args) - 1
87+
88+
for index, entry in enumerate(split_args):
89+
entry_length = len(entry)
90+
if entry_length == 1:
91+
if index == index_of_last_entry:
92+
boolean_arguments.append(entry)
93+
else:
94+
add_string_argument(entry)
95+
elif entry_length > 1:
96+
entry_arguments = list(entry)
97+
# the last item is a string entry
98+
add_string_argument(entry_arguments.pop())
99+
# the other items must not have arguments i.e. are booleans
100+
for item in entry_arguments:
101+
if item == 'h':
102+
continue
103+
boolean_arguments.append(item)
104+
else:
105+
continue
106+
107+
return {
108+
'booleans': boolean_arguments,
109+
'integers': [],
110+
'strings': string_arguments,
111+
}
112+
113+
114+
def _minimist_from_getopt(prog, description, getopt_string, help_by_argument, name_by_argument):
115+
return minimist(prog, description, is_getopt=True,
116+
help_by_argument=help_by_argument, name_by_argument=name_by_argument,
117+
**_parse_getopt_string(getopt_string))
118+
119+
120+
def break_apart_legacy_usage(value):
121+
lines = value.split('\n')
122+
line_parts = (line.split(':') for line in lines if line)
123+
return dict(((k.lstrip()[1:], v.strip()) for k, v in line_parts))
124+
125+
126+
def parse_getopt_args(argv, getopt_string, prog="", description="", help_by_argument=_EMPTY_DICT, name_by_argument=_EMPTY_DICT):
127+
arg_parser = _minimist_from_getopt(
128+
prog, description, getopt_string,
129+
help_by_argument=help_by_argument, name_by_argument=name_by_argument)
130+
opts = arg_parser.parse_args(argv, namespace=GetoptCompatNamespace())
131+
try:
132+
args = getattr(opts, _POSITIONAL_MARKER)
133+
except AttributeError:
134+
args = []
135+
return (opts, args)

tests/test_mig_shared_minimist.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
from __future__ import print_function
2+
import sys
3+
4+
from tests.support import MigTestCase, testmain
5+
6+
from mig.shared.minimist import parse_getopt_args
7+
8+
9+
class TestMigSharedMinimist__getopt(MigTestCase):
10+
def test_arbitrary_arguments(self):
11+
(opts, args) = parse_getopt_args(["arg1", "arg2", "arg3"], "")
12+
13+
self.assertEqual(dict(opts), {})
14+
self.assertEqual(args, ['arg1', 'arg2', 'arg3'])
15+
16+
def test_boolean_arguments(self):
17+
(opts, args) = parse_getopt_args(["-a", "-b", "-c"], "x:abcy:")
18+
19+
self.assertEqual(dict(opts), {
20+
'-a': True,
21+
'-b': True,
22+
'-c': True,
23+
})
24+
self.assertEqual(args, [])
25+
26+
27+
def test_final_argument_is_boolean(self):
28+
(opts, args) = parse_getopt_args(["-b", "arg1", "arg2", "arg3"], "o:b")
29+
30+
self.assertEqual(dict(opts), {
31+
'-b': True,
32+
})
33+
self.assertEqual(args, ['arg1', 'arg2', 'arg3'])
34+
35+
def test_single_argument_is_boolean(self):
36+
(opts, args) = parse_getopt_args(["-b", "arg1", "arg2", "arg3"], "b")
37+
38+
self.assertEqual(dict(opts), {
39+
'-b': True,
40+
})
41+
self.assertEqual(args, ['arg1', 'arg2', 'arg3'])
42+
43+
44+
if __name__ == '__main__':
45+
testmain()

0 commit comments

Comments
 (0)