-
Notifications
You must be signed in to change notification settings - Fork 17
/
input_fields.py
327 lines (266 loc) · 10 KB
/
input_fields.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
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
import click
from click_web.web_click_types import (EmailParamType, PasswordParamType,
TextAreaParamType)
class FieldId:
"""
Extract/serialize information from the encoded form input field name
the parts:
[command_index].[opt_or_arg_index].[click_type].[html_input_type].[opt_or_arg_name]
e.g.
"0.0.option.text.text.--an-option"
"0.1.argument.file[rb].text.an-argument"
"""
SEPARATOR = '.'
def __init__(self,
command_index,
param_index,
param_type,
click_type,
nargs,
form_type,
name,
key=None):
'the int index of the command it belongs to'
self.command_index = int(command_index)
'the int index for the ordering of paramters/arguments'
self.param_index = int(param_index)
'Type of option (argument, option, flag)'
self.param_type = param_type
'Type of option (file, text)'
self.click_type = click_type
'nargs value (-1 is variardic)'
self.nargs = int(nargs)
'Type of html input type'
self.form_type = form_type
'The actual command line option (--debug)'
self.name = name
'The actual form id'
self.key = key if key else str(self)
def __str__(self):
return self.SEPARATOR.join(str(p) for p in (self.command_index,
self.param_index,
self.param_type,
self.click_type,
self.nargs,
self.form_type,
self.name))
@classmethod
def from_string(cls, field_info_as_string) -> 'FieldId':
args = field_info_as_string.split(cls.SEPARATOR) + [field_info_as_string, ]
return cls(*args)
class NotSupported(ValueError):
pass
class BaseInput:
param_type_cls = None
def __init__(self, ctx, param: click.Parameter, command_index, param_index):
self.ctx = ctx
self.param = param
self.command_index = command_index
self.param_index = param_index
if not self.is_supported():
raise NotSupported()
def is_supported(self):
return isinstance(self.param.type, self.param_type_cls)
@property
def fields(self) -> dict:
field = {}
param = self.param
field['param'] = param.param_type_name
if param.param_type_name == 'option':
name = self._to_cmd_line_name(param.opts[0])
field['value'] = param.default if param.default else ''
field['checked'] = 'checked="checked"' if param.default else ''
field['desc'] = param.help
field['help'] = param.get_help_record(self.ctx)
elif param.param_type_name == 'argument':
name = self._to_cmd_line_name(param.name)
field['value'] = param.default
field['checked'] = ''
field['help'] = ''
field['name'] = self._build_name(name)
field['required'] = param.required
# if param.nargs < 0:
# raise exceptions.ClickWebException("Parameters with unlimited nargs not supportet at the moment.")
field['nargs'] = param.nargs
field['human_readable_name'] = param.human_readable_name.replace('_', ' ')
field.update(self.type_attrs)
return field
@property
def type_attrs(self) -> dict:
"""
Return the input type and type specific information as dict
"""
raise NotImplementedError()
def _to_cmd_line_name(self, name: str) -> str:
return name.replace('_', '-')
def _build_name(self, name: str):
"""
Construct a name to use for field in form that have information about
what sub-command it belongs to, order index (for later sorting) and type of parameter.
"""
# get the type of param to encode the in the name
if self.param.param_type_name == 'option':
param_type = 'flag' if self.param.is_bool_flag else 'option'
else:
param_type = self.param.param_type_name
click_type = self.type_attrs['click_type']
form_type = self.type_attrs['type']
# in order for the form to have arguments for sub commands we need to add the
# index of the command the argument it belongs to.
return str(FieldId(self.command_index,
self.param_index,
param_type,
click_type,
self.param.nargs,
form_type,
name))
class ChoiceInput(BaseInput):
param_type_cls = click.Choice
@property
def type_attrs(self):
type_attrs = {}
type_attrs['type'] = 'option'
type_attrs['options'] = self.param.type.choices
type_attrs['default'] = self.param.default
type_attrs['click_type'] = 'choice'
return type_attrs
class FlagInput(BaseInput):
def is_supported(self, ):
return self.param.param_type_name == 'option' and self.param.is_bool_flag
@property
def type_attrs(self):
type_attrs = {}
type_attrs['type'] = 'checkbox'
type_attrs['click_type'] = 'bool_flag'
type_attrs['value'] = self.param.opts[0]
# the "on" value e.g ['--flag']
type_attrs['on_flag'] = self.param.opts[0]
# the "off" value e.g ['--no-flag']
if self.param.secondary_opts:
type_attrs['off_flag'] = self.param.secondary_opts[0]
return type_attrs
class IntInput(BaseInput):
param_type_cls = click.types.IntParamType
@property
def type_attrs(self):
type_attrs = {}
type_attrs['type'] = 'number'
type_attrs['step'] = '1'
type_attrs['click_type'] = 'int'
return type_attrs
class FloatInput(BaseInput):
param_type_cls = click.types.FloatParamType
@property
def type_attrs(self):
type_attrs = {}
type_attrs['type'] = 'number'
type_attrs['step'] = 'any'
type_attrs['click_type'] = 'float'
return type_attrs
class FolderInput(BaseInput):
def is_supported(self):
if isinstance(self.param.type, click.Path):
if self.param.type.dir_okay:
return True
return False
@property
def type_attrs(self):
type_attrs = {}
# if it is required we treat it as an input folder
# and only accept zip.
mode = 'r' if self.param.type.exists else 'w'
type_attrs['click_type'] = f'path[{mode}]'
if self.param.type.exists:
type_attrs['accept'] = "application/zip"
type_attrs['type'] = 'file'
else:
type_attrs['type'] = 'hidden'
return type_attrs
class FileInput(BaseInput):
def is_supported(self):
if isinstance(self.param.type, click.File):
return True
elif isinstance(self.param.type, click.Path):
if (self.param.type.file_okay):
return True
return False
@property
def type_attrs(self):
type_attrs = {}
if isinstance(self.param.type, click.File):
mode = self.param.type.mode
elif isinstance(self.param.type, click.Path):
mode = 'w' if self.param.type.writable else ''
mode += 'r' if self.param.type.readable else ''
else:
raise NotSupported(f'Illegal param type. Got type: {self.param.type}')
type_attrs['click_type'] = f'file[{mode}]'
if 'r' not in mode:
if self.param.required:
# if file is only for output do not show in form
type_attrs['type'] = 'hidden'
else:
type_attrs['type'] = 'text'
else:
type_attrs['type'] = 'file'
return type_attrs
class EmailInput(BaseInput):
param_type_cls = EmailParamType
@property
def type_attrs(self):
type_attrs = {}
type_attrs['type'] = 'email'
type_attrs['click_type'] = 'email'
return type_attrs
class PasswordInput(BaseInput):
param_type_cls = PasswordParamType
@property
def type_attrs(self):
type_attrs = {}
type_attrs["type"] = "password"
type_attrs["click_type"] = "password"
return type_attrs
class TextAreaInput(BaseInput):
param_type_cls = TextAreaParamType
@property
def type_attrs(self):
type_attrs = {}
type_attrs['type'] = 'textarea'
type_attrs['click_type'] = 'textarea'
return type_attrs
class DefaultInput(BaseInput):
param_type_cls = click.ParamType
@property
def type_attrs(self):
type_attrs = {}
type_attrs['type'] = 'text'
type_attrs['click_type'] = 'text'
return type_attrs
'''
The types of inputs we support. Form inputs listed in priority order, first that matches will be selected.
To add new Input handling in html forms for custom Parameter types just Subclass BaseInput and insert
the class in the list.
'''
INPUT_TYPES = [ChoiceInput,
FlagInput,
IntInput,
FloatInput,
FolderInput,
FileInput,
EmailInput,
PasswordInput,
TextAreaInput]
_DEFAULT_INPUT = [DefaultInput]
def get_input_field(ctx: click.Context, param: click.Parameter, command_index, param_index) -> dict:
"""
Convert a click.Parameter into a dict structure describing a html form option
"""
for input_cls in INPUT_TYPES + _DEFAULT_INPUT:
try:
input_type = input_cls(ctx, param, command_index, param_index)
except NotSupported:
pass
else:
fields = input_type.fields
return fields
raise NotSupported(f"No Form input type not supported: {param}")