-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcommand_generation.py
More file actions
219 lines (168 loc) · 7.08 KB
/
command_generation.py
File metadata and controls
219 lines (168 loc) · 7.08 KB
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
import click
from collections import namedtuple, OrderedDict
from autorepr import autorepr
from copy import deepcopy
def generate_commands(root_command, config, callback):
"""Generate commands using the given config.
Recursively define a tree of commands using the config, where the leaf
commands will execute the callback function with 3 arguments when run: a
list of the ancestor commands, a list of the arguments, and a dict of
passed option names to values. The generated commands will be added to the
given root command.
"""
for command_name, config in config.items():
initial_ancestor_commands = [command_name]
command = _parse_command(initial_ancestor_commands, config, callback)
root_command.add_command(command)
def _parse_command(ancestor_commands, config, callback):
if 'commands' in config:
command_class = click.Group
config_parser = _parse_group_command_config
else:
command_class = click.Command
config_parser = _parse_simple_command_config
command_args = config_parser(ancestor_commands, config, callback)
command_name = ancestor_commands[-1]
command = command_class(
command_name,
help=config.get('help'),
short_help=(config.get('short_help') or config.get('help')),
**command_args
)
return command
class Options():
# An additional option to be passed to the callback function, not created
# from a click.Option or passed a value (directly) by a user; can create
# via `__setitem__`.
AdditionalOption = namedtuple('AdditionalOption', ['value'])
fields = ['options_map']
__repr__ = autorepr(fields)
def __init__(self, options_config):
self.options_map = {}
for name_or_names, config in options_config.items():
names = _to_list(name_or_names)
click_option = click.Option(names, **config)
# Put Option entry in map for each option name, as options can have
# multiple names but we want to be able to navigate to click Option
# and option config from any of these.
for name in names:
option = Option(name, config, click_option)
self.options_map[option.identifier()] = option
def __getitem__(self, identifier):
return self.options_map[identifier]
def __setitem__(self, identifier, option_value):
self.options_map[identifier] = Options.AdditionalOption(option_value)
def __delitem__(self, identifier):
del self.options_map[identifier]
def __iter__(self):
return iter(self.options_map.items())
def click_options(self):
"""`click.Option`s for these Options"""
# Note: get click.Options as set before converting to list to remove
# duplicates; same option will appear in map multiple times for each
# name for the option, but we want only one copy of each.
return list({opt.click_option for opt in self.options_map.values()})
def values_map(self):
"""A map from option identifiers to values, without unpassed options"""
return {
identifier: option.value
for identifier, option in self
# Don't include not passed options or flags.
if option.value not in [None, False]
}
class Option():
fields = [
'click_option',
'config',
'parameter',
'value'
]
__repr__ = autorepr(fields)
def __init__(self, parameter, config, click_option):
self.parameter = parameter
self.config = config
self.click_option = click_option
self.value = None
def identifier(self):
return _parameter_identifier(self.parameter)
def _parse_simple_command_config(ancestor_commands, config, callback):
arguments_config = config.get('arguments', [])
options_config = config.get('options', {})
# Create ordered map of argument identifiers to click.Arguments.
# TODO Could create Argument and Arguments classes as with Options; don't
# need complicated behaviour (yet) for arguments but might make things
# clearer.
arguments = _form_arguments(arguments_config)
click_arguments = list(arguments.values())
options = Options(options_config)
click_params = click_arguments + options.click_options()
# Define function to be passed as 'callback' parameter to click.Command,
# transforming its arguments suitable to be passed to our own callback.
def click_callback(ctx, **params):
argument_values = [
params[arg_name] for arg_name in arguments.keys()
]
# Set values for passed options.
passed_param_names = params.keys()
for identifier, option in options:
if identifier in passed_param_names:
option.value = params[identifier]
# Clone arguments for callback so callback can modify freely without
# effecting future callback calls.
new_commands = deepcopy(ancestor_commands)
new_options = deepcopy(options)
callback_args = [new_commands, argument_values, new_options]
if config.get('pass_context'):
callback(ctx, *callback_args)
else:
callback(*callback_args)
return {
'params': click_params,
'callback': click.pass_context(click_callback)
}
def _form_arguments(arguments_config):
args_map = OrderedDict()
is_required = True
for arg_n in arguments_config:
def add_argument(name):
identifier = _parameter_identifier(name)
args_map[identifier] = click.Argument([name], required = is_required)
if isinstance(arg_n, list):
# Sublists flag all future arguments as optional
is_required = False
for n in arg_n: add_argument(n)
else:
add_argument(arg_n)
return args_map
def _to_list(obj):
if isinstance(obj, str):
# Create 1-element list, otherwise would be converted to list of chars.
return [obj]
else:
return list(obj)
def _parameter_identifier(param):
# Click strips leading dashes and translates internal dashes to underscores
# in passed parameter names to get valid variable names to pass to command
# callbacks, so to get the parameter identifiers which will be used by
# Click we need to perform this translation ourselves.
translation_table = str.maketrans('-', '_')
return param.lstrip('-').translate(translation_table)
def _parse_group_command_config(ancestor_commands, config, callback):
commands = {
command_name: _parse_command(
ancestor_commands + [command_name],
config,
callback
)
for command_name, config
in config['commands'].items()
}
config.setdefault('invoke_without_command', False)
return_hash = { 'commands': commands }
if config['invoke_without_command']:
return_hash['invoke_without_command'] = True
return_hash = {
**return_hash,
**_parse_simple_command_config(ancestor_commands, config, callback)
}
return return_hash