-
Notifications
You must be signed in to change notification settings - Fork 3
/
cli_tools.py
274 lines (235 loc) · 8.77 KB
/
cli_tools.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
""" Functions for working with command-line interaction """
from .collection import is_collection_like, merge_dicts
from argparse import (
ArgumentParser,
_SubParsersAction,
_HelpAction,
_VersionAction,
SUPPRESS,
)
import sys
__classes__ = ["VersionInHelpParser"]
__all__ = __classes__ + ["build_cli_extra", "query_yes_no", "convert_value"]
class VersionInHelpParser(ArgumentParser):
def __init__(self, version=None, **kwargs):
"""Overwrites the inherited init. Saves the version as an object
attribute for further use."""
super(VersionInHelpParser, self).__init__(**kwargs)
self.version = version
if self.version is not None:
self.add_argument(
"--version",
action="version",
version="%(prog)s {}".format(self.version),
)
def format_help(self):
""" Add version information to help text. """
help_string = (
"version: {}\n".format(str(self.version))
if self.version is not None
else ""
)
return help_string + super(VersionInHelpParser, self).format_help()
def subparsers(self):
"""
Get the subparser associated with a parser.
:return argparse._SubparsersAction: action defining the subparsers
"""
subs = [a for a in self._actions if isinstance(a, _SubParsersAction)]
if len(subs) != 1:
raise ValueError("Expected exactly 1 subparser, got {}".format(len(subs)))
return subs[0]
def top_level_args(self):
"""
Get actions not assiated with any subparser.
Help and version are also excluded
:return list[argparse.<action_type>]: list of argument actions
"""
excl = [_SubParsersAction, _HelpAction, _VersionAction]
return [a for a in self._actions if not type(a) in excl]
def subcommands(self):
"""
Get subcommands defined by a parser.
:return list[str]: subcommands defined within this parser
"""
return list(self.subparsers().choices.keys())
def dests_by_subparser(self, subcommand=None, top_level=False):
"""
Get argument dests by subcommand from a parser.
:param str subcommand: subcommand to get dests for
:return dict: dests by subcommand
"""
if top_level:
top_level_actions = self.top_level_args()
dest_list = []
for tla in top_level_actions:
if hasattr(tla, "dest"):
dest_list.append(tla.dest)
return dest_list
if subcommand is not None and subcommand not in self.subcommands():
raise ValueError(
"'{}' not found in this parser commands: {}".format(
subcommand, str(self.subcommands())
)
)
subs = (
self.subparsers().choices
if subcommand is None
else {subcommand: self.subparsers().choices[subcommand]}
)
dests = {}
for subcmd, sub in subs.items():
dest_list = []
for action in sub._actions:
if isinstance(action, _HelpAction):
continue
if hasattr(action, "dest"):
dest_list.append(action.dest)
dests[subcmd] = dest_list
return dests
def suppress_defaults(self):
"""
Remove parser change defaults to argparse.SUPPRESS so that they do not
show up in the argparse.Namespace object after argument parsing.
"""
top_level_actions = self.top_level_args()
for tla in top_level_actions:
if hasattr(tla, "dest"):
tla.dest = SUPPRESS
subs = self.subparsers().choices
for subcmd, sub in subs.items():
for sa in sub._actions:
if hasattr(sa, "dest"):
sa.default = SUPPRESS
def arg_defaults(self, subcommand=None, unique=False, top_level=False):
"""
Get argument defaults by subcommand from a parser.
:param str subcommand: subcommand to get defaults for
:param bool unique: whether only unique flat dict of dests and
defaults mapping should be returned
:return dict: defaults by subcommand
"""
if top_level:
top_level_actions = self.top_level_args()
defaults_dict = {}
for tla in top_level_actions:
if hasattr(tla, "default") and hasattr(tla, "dest"):
defaults_dict.update({tla.dest: tla.default})
return defaults_dict
if subcommand is not None and subcommand not in self.subcommands():
raise ValueError(
"'{}' not found in this parser commands: {}".format(
subcommand, str(self.subcommands())
)
)
subs = (
self.subparsers().choices
if subcommand is None
else {subcommand: self.subparsers().choices[subcommand]}
)
defaults = {}
for subcmd, sub in subs.items():
defaults_dict = {}
for action in sub._actions:
if isinstance(action, _HelpAction):
continue
if hasattr(action, "default") and hasattr(action, "dest"):
defaults_dict.update({action.dest: action.default})
defaults[subcmd] = defaults_dict
if unique:
unique_defaults = {}
for k, v in defaults.items():
unique_defaults = merge_dicts(unique_defaults, v)
return unique_defaults
return defaults
def build_cli_extra(optargs):
"""
Render CLI options/args as text to add to base command.
To specify a flag, map an option to None. Otherwise, map option short or
long name to value(s). Values that are collection types will be rendered
with single space between each. All non-string values are converted to
string.
:param Mapping | Iterable[(str, object)] optargs: values used as
options/arguments
:return str: text to add to base command, based on given opts/args
:raise TypeError: if an option name isn't a string
"""
def render(k, v):
if not isinstance(k, str):
raise TypeError("Option name isn't a string: {} ({})".format(k, type(k)))
if v is None:
return k
if is_collection_like(v):
v = " ".join(map(str, v))
return "{} {}".format(k, v)
try:
data_iter = optargs.items()
except AttributeError:
data_iter = optargs
return " ".join(render(*kv) for kv in data_iter)
def query_yes_no(question, default="no"):
"""
Ask a yes/no question via raw_input() and return their answer.
:param str question: a string that is presented to the user.
:param str default: the presumed answer if the user just hits <Enter>.
:return bool: True for "yes" or False for "no"
"""
def parse(ans):
return {"yes": True, "y": True, "ye": True, "no": False, "n": False}[
ans.lower()
]
try:
prompt = {None: "[y/n]", "yes": "[Y/n]", "no": "[y/N]"}[
None if default is None else default.lower()
]
except (AttributeError, KeyError):
raise ValueError("invalid default answer: {}".format(default))
msg = "{q} {p} ".format(q=question, p=prompt)
while True:
sys.stdout.write(msg)
try:
return parse(_read_from_user() or default)
except KeyError:
sys.stdout.write("Please respond with 'yes' or 'no' (or 'y' or 'n').\n")
def convert_value(val):
"""
Convert string to the most appropriate type, one of:
bool, str, int, None or float
:param str val: the string to convert
:return bool | str | int | float | None: converted string to the
most appropriate type
"""
if not isinstance(val, str):
try:
val = str(val)
except:
raise ValueError(
"The input has to be of type convertible to 'str',"
" got '{}'".format(type(val))
)
if isinstance(val, bool):
return val
if isinstance(val, str):
if val == "None":
return None
if val.lower() == "true":
return True
if val.lower() == "false":
return False
try:
float(val)
except ValueError:
return val
else:
try:
int(val)
except ValueError:
return float(val)
else:
return int(val)
def _read_from_user():
import sys
if sys.version_info.major < 3:
from __builtin__ import raw_input
return raw_input()
return input()