Skip to content

Commit 2f3cfa8

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 2f3cfa8

File tree

2 files changed

+198
-0
lines changed

2 files changed

+198
-0
lines changed

mig/shared/arguments.py

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

tests/test_mig_shared_arguments.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.arguments import parse_getopt_args
7+
8+
9+
class TestMigSharedArguments__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)