1
|
|
|
# -*- coding: utf-8 -*- |
2
|
|
|
# flake8: noqa |
3
|
|
|
""" |
4
|
|
|
cookiecutter.context |
5
|
|
|
-------------------- |
6
|
|
|
|
7
|
|
|
Process the version 2 cookiecutter context (previsously loaded via |
8
|
|
|
cookiecutter.json) and handle any user input that might be associated with |
9
|
|
|
initializing the settings defined in the 'variables' OrderedDict part of the |
10
|
|
|
context. |
11
|
|
|
|
12
|
|
|
This module produces a dictionary used later by the jinja2 template engine to |
13
|
|
|
generate files. |
14
|
|
|
|
15
|
|
|
Based on the source code written by @hackebrot see: |
16
|
|
|
https://github.com/audreyr/cookiecutter/pull/848 |
17
|
|
|
https://github.com/hackebrot/cookiecutter/tree/new-context-format |
18
|
|
|
|
19
|
|
|
""" |
20
|
|
|
|
21
|
|
|
import logging |
22
|
|
|
import collections |
23
|
|
|
import json |
24
|
|
|
import re |
25
|
|
|
|
26
|
|
|
import click |
27
|
|
|
from jinja2 import Environment |
28
|
|
|
|
29
|
|
|
logger = logging.getLogger(__name__) |
30
|
|
|
|
31
|
|
|
DEFAULT_PROMPT = 'Please enter a value for "{variable.name}"' |
32
|
|
|
|
33
|
|
|
VALID_TYPES = [ |
34
|
|
|
'boolean', |
35
|
|
|
'yes_no', |
36
|
|
|
'int', |
37
|
|
|
'float', |
38
|
|
|
'uuid', |
39
|
|
|
'json', |
40
|
|
|
'string', |
41
|
|
|
] |
42
|
|
|
|
43
|
|
|
SET_OF_REQUIRED_FIELDS = { |
44
|
|
|
'name', |
45
|
|
|
'cookiecutter_version', |
46
|
|
|
'variables', |
47
|
|
|
} |
48
|
|
|
|
49
|
|
|
REGEX_COMPILE_FLAGS = { |
50
|
|
|
'ascii': re.ASCII, |
51
|
|
|
'debug': re.DEBUG, |
52
|
|
|
'ignorecase': re.IGNORECASE, |
53
|
|
|
'locale': re.LOCALE, |
54
|
|
|
'mulitline': re.MULTILINE, |
55
|
|
|
'dotall': re.DOTALL, |
56
|
|
|
'verbose': re.VERBOSE, |
57
|
|
|
} |
58
|
|
|
|
59
|
|
|
|
60
|
|
|
def context_is_version_2(cookiecutter_context): |
61
|
|
|
""" |
62
|
|
|
Return True if the cookiecutter_context meets the current requirements for |
63
|
|
|
a version 2 cookiecutter.json file format. |
64
|
|
|
""" |
65
|
|
|
# This really is not sufficient since a v1 context could define each of |
66
|
|
|
# these fields; perhaps a more thorough test would be to also check if the |
67
|
|
|
# 'variables' field was defined as a list of OrderedDict items. |
68
|
|
|
if (cookiecutter_context.keys() & |
69
|
|
|
SET_OF_REQUIRED_FIELDS) == SET_OF_REQUIRED_FIELDS: |
70
|
|
|
return True |
71
|
|
|
else: |
72
|
|
|
return False |
73
|
|
|
|
74
|
|
|
|
75
|
|
|
def prompt_string(variable, default): |
76
|
|
|
return click.prompt( |
77
|
|
|
variable.prompt, |
78
|
|
|
default=default, |
79
|
|
|
hide_input=variable.hide_input, |
80
|
|
|
type=click.STRING, |
81
|
|
|
) |
82
|
|
|
|
83
|
|
|
|
84
|
|
|
def prompt_boolean(variable, default): |
85
|
|
|
return click.prompt( |
86
|
|
|
variable.prompt, |
87
|
|
|
default=default, |
88
|
|
|
hide_input=variable.hide_input, |
89
|
|
|
type=click.BOOL, |
90
|
|
|
) |
91
|
|
|
|
92
|
|
|
|
93
|
|
|
def prompt_int(variable, default): |
94
|
|
|
return click.prompt( |
95
|
|
|
variable.prompt, |
96
|
|
|
default=default, |
97
|
|
|
hide_input=variable.hide_input, |
98
|
|
|
type=click.INT, |
99
|
|
|
) |
100
|
|
|
|
101
|
|
|
|
102
|
|
|
def prompt_float(variable, default): |
103
|
|
|
return click.prompt( |
104
|
|
|
variable.prompt, |
105
|
|
|
default=default, |
106
|
|
|
hide_input=variable.hide_input, |
107
|
|
|
type=click.FLOAT, |
108
|
|
|
) |
109
|
|
|
|
110
|
|
|
|
111
|
|
|
def prompt_uuid(variable, default): |
112
|
|
|
return click.prompt( |
113
|
|
|
variable.prompt, |
114
|
|
|
default=default, |
115
|
|
|
hide_input=variable.hide_input, |
116
|
|
|
type=click.UUID, |
117
|
|
|
) |
118
|
|
|
|
119
|
|
|
|
120
|
|
|
def prompt_json(variable, default): |
121
|
|
|
# The JSON object from cookiecutter.json might be very large |
122
|
|
|
# We only show 'default' |
123
|
|
|
DEFAULT_JSON = 'default' |
124
|
|
|
|
125
|
|
|
def process_json(user_value): |
126
|
|
|
try: |
127
|
|
|
return json.loads( |
128
|
|
|
user_value, |
129
|
|
|
object_pairs_hook=collections.OrderedDict, |
130
|
|
|
) |
131
|
|
|
except ValueError: |
132
|
|
|
# json.decoder.JSONDecodeError raised in Python 3.5, 3.6 |
133
|
|
|
# but it inherits from ValueError which is raised in Python 3.4 |
134
|
|
|
# --------------------------------------------------------------- |
135
|
|
|
# Leave it up to click to ask the user again. |
136
|
|
|
# Local function procsse_json() is called by click within a |
137
|
|
|
# try block that catches click.UsageError exception's and asks |
138
|
|
|
# the user to try again. |
139
|
|
|
raise click.UsageError('Unable to decode to JSON.') |
140
|
|
|
|
141
|
|
|
dict_value = click.prompt( |
142
|
|
|
variable.prompt, |
143
|
|
|
default=DEFAULT_JSON, |
144
|
|
|
hide_input=variable.hide_input, |
145
|
|
|
type=click.STRING, |
146
|
|
|
value_proc=process_json, |
147
|
|
|
) |
148
|
|
|
|
149
|
|
|
if dict_value == DEFAULT_JSON: |
150
|
|
|
# Return the given default w/o any processing |
151
|
|
|
return default |
152
|
|
|
return dict_value |
153
|
|
|
|
154
|
|
|
|
155
|
|
|
def prompt_yes_no(variable, default): |
156
|
|
|
if default is True: |
157
|
|
|
default_display = 'y' |
158
|
|
|
else: |
159
|
|
|
default_display = 'n' |
160
|
|
|
|
161
|
|
|
# click.prompt() behavior: |
162
|
|
|
# When supplied with a string default, the string default is returned, |
163
|
|
|
# rather than the string converted to a click.BOOL. |
164
|
|
|
# If default is passed as a boolean then the default is displayed as |
165
|
|
|
# [True] or [False], rather than [y] or [n]. |
166
|
|
|
# This prompt translates y, yes, Yes, YES, n, no, No, NO to their correct |
167
|
|
|
# boolean values, its just that it does not translate a string default |
168
|
|
|
# value of y, yes, Yes, YES, n, no, No, NO to a boolean... |
169
|
|
|
value = click.prompt( |
170
|
|
|
variable.prompt, |
171
|
|
|
default=default_display, |
172
|
|
|
hide_input=variable.hide_input, |
173
|
|
|
type=click.BOOL, |
174
|
|
|
) |
175
|
|
|
|
176
|
|
|
# ...so if we get the displayed default value back (its a string), |
177
|
|
|
# change it to its associated boolean value |
178
|
|
|
if value == default_display: |
179
|
|
|
value = default |
180
|
|
|
|
181
|
|
|
return value |
182
|
|
|
|
183
|
|
|
|
184
|
|
|
def prompt_choice(variable, default): |
185
|
|
|
"""Returns prompt, default and callback for a choice variable""" |
186
|
|
|
choice_map = collections.OrderedDict( |
187
|
|
|
(u'{}'.format(i), value) |
188
|
|
|
for i, value in enumerate(variable.choices, 1) |
189
|
|
|
) |
190
|
|
|
choices = choice_map.keys() |
191
|
|
|
|
192
|
|
|
prompt = u'\n'.join(( |
193
|
|
|
variable.prompt, |
194
|
|
|
u'\n'.join([u'{} - {}'.format(*c) for c in choice_map.items()]), |
195
|
|
|
u'Choose from {}'.format(u', '.join(choices)), |
196
|
|
|
)) |
197
|
|
|
default = str(variable.choices.index(default) + 1) |
198
|
|
|
|
199
|
|
|
user_choice = click.prompt( |
200
|
|
|
prompt, |
201
|
|
|
default=default, |
202
|
|
|
hide_input=variable.hide_input, |
203
|
|
|
type=click.Choice(choices), |
204
|
|
|
) |
205
|
|
|
return choice_map[user_choice] |
206
|
|
|
|
207
|
|
|
|
208
|
|
|
PROMPTS = { |
209
|
|
|
'string': prompt_string, |
210
|
|
|
'boolean': prompt_boolean, |
211
|
|
|
'int': prompt_int, |
212
|
|
|
'float': prompt_float, |
213
|
|
|
'uuid': prompt_uuid, |
214
|
|
|
'json': prompt_json, |
215
|
|
|
'yes_no': prompt_yes_no, |
216
|
|
|
} |
217
|
|
|
|
218
|
|
|
|
219
|
|
|
def deserialize_string(value): |
220
|
|
|
return str(value) |
221
|
|
|
|
222
|
|
|
|
223
|
|
|
def deserialize_boolean(value): |
224
|
|
|
return bool(value) |
225
|
|
|
|
226
|
|
|
|
227
|
|
|
def deserialize_yes_no(value): |
228
|
|
|
return bool(value) |
229
|
|
|
|
230
|
|
|
|
231
|
|
|
def deserialize_int(value): |
232
|
|
|
return int(value) |
233
|
|
|
|
234
|
|
|
|
235
|
|
|
def deserialize_float(value): |
236
|
|
|
return float(value) |
237
|
|
|
|
238
|
|
|
|
239
|
|
|
def deserialize_uuid(value): |
240
|
|
|
return click.UUID(value) |
241
|
|
|
|
242
|
|
|
|
243
|
|
|
def deserialize_json(value): |
244
|
|
|
return value |
245
|
|
|
|
246
|
|
|
|
247
|
|
|
DESERIALIZERS = { |
248
|
|
|
'string': deserialize_string, |
249
|
|
|
'boolean': deserialize_boolean, |
250
|
|
|
'int': deserialize_int, |
251
|
|
|
'float': deserialize_float, |
252
|
|
|
'uuid': deserialize_uuid, |
253
|
|
|
'json': deserialize_json, |
254
|
|
|
'yes_no': deserialize_yes_no, |
255
|
|
|
} |
256
|
|
|
|
257
|
|
|
|
258
|
|
|
class Variable(object): |
259
|
|
|
""" |
260
|
|
|
Embody attributes of variables while processing the variables field of |
261
|
|
|
a cookiecutter version 2 context. |
262
|
|
|
""" |
263
|
|
|
|
264
|
|
|
def __init__(self, name, default, **info): |
265
|
|
|
""" |
266
|
|
|
:param name: A string containing the variable's name in the jinja2 |
267
|
|
|
context. |
268
|
|
|
:param default: The variable's default value. Can any type defined |
269
|
|
|
below. |
270
|
|
|
:param kwargs info: Keyword/Argument pairs recognized are shown below. |
271
|
|
|
|
272
|
|
|
Recognized Keyword/Arguments, but optional: |
273
|
|
|
|
274
|
|
|
- `description` -- A string description of the variable. |
275
|
|
|
- `prompt` -- A string to show user when prompted for input. |
276
|
|
|
- `prompt_user` -- A boolean, if True prompt user; else no prompt. |
277
|
|
|
- `hide_input` -- A boolean, if True hide user's input. |
278
|
|
|
- `type` -- Specifies the variable's data type see below, |
279
|
|
|
defaults to string. |
280
|
|
|
- `skip_if` -- A string of a jinja2 renderable boolean expression, |
281
|
|
|
the variable will be skipped if it renders True. |
282
|
|
|
- `do_if` -- A string of a jinja2 renderable boolean expression, |
283
|
|
|
the variable will be processed if it renders True. |
284
|
|
|
- `choices` -- A list of choices, may be of mixed types. |
285
|
|
|
- `if_yes_skip_to` -- A string containing a variable name to skip |
286
|
|
|
to if the yes_no value is True (yes). Only has meaning for |
287
|
|
|
variables of type 'yes_no'. |
288
|
|
|
- `if_no_skip_to` -- A string containing a variable name to skip |
289
|
|
|
to if the yes_no value is False (no). Only has meaning for |
290
|
|
|
variables of type 'yes_no'. |
291
|
|
|
- `validation` -- A string defining a regex to use to validation |
292
|
|
|
user input. Defaults to None. |
293
|
|
|
- `validation_msg` -- A string defining an additional message to |
294
|
|
|
display if the validation check fails. |
295
|
|
|
- `validation_flags` -- A list of validation flag names that can be |
296
|
|
|
specified to control the behaviour of the validation |
297
|
|
|
check done using the above defined `validation` string. |
298
|
|
|
Specifying a flag is equivalent to setting it to True, |
299
|
|
|
not specifying a flag is equivalent to setting it to False. |
300
|
|
|
The default value of this variable has no effect on the |
301
|
|
|
validation check. |
302
|
|
|
|
303
|
|
|
The flags supported are: |
304
|
|
|
|
305
|
|
|
* ascii - enabling re.ASCII |
306
|
|
|
* debug - enabling re.DEBUG |
307
|
|
|
* ignorecase - enabling re.IGNORECASE |
308
|
|
|
* locale - enabling re.LOCALE |
309
|
|
|
* mulitline - enabling re.MULTILINE |
310
|
|
|
* dotall - enabling re.DOTALL |
311
|
|
|
* verbose - enabling re.VERBOSE |
312
|
|
|
|
313
|
|
|
See: https://docs.python.org/3/library/re.html#re.compile |
314
|
|
|
|
315
|
|
|
Supported Types |
316
|
|
|
* string |
317
|
|
|
* boolean |
318
|
|
|
* int |
319
|
|
|
* float |
320
|
|
|
* uuid |
321
|
|
|
* json |
322
|
|
|
* yes_no |
323
|
|
|
|
324
|
|
|
""" |
325
|
|
|
|
326
|
|
|
# mandatory fields |
327
|
|
|
self.name = name |
328
|
|
|
self.default = default |
329
|
|
|
|
330
|
|
|
# optional fields |
331
|
|
|
self.info = info |
332
|
|
|
|
333
|
|
|
# -- DESCRIPTION ----------------------------------------------------- |
334
|
|
|
self.description = self.check_type('description', None, str) |
335
|
|
|
|
336
|
|
|
# -- PROMPT ---------------------------------------------------------- |
337
|
|
|
self.prompt = self.check_type('prompt', DEFAULT_PROMPT.format(variable=self), str) |
338
|
|
|
|
339
|
|
|
# -- HIDE_INPUT ------------------------------------------------------ |
340
|
|
|
self.hide_input = self.check_type('hide_input', False, bool) |
341
|
|
|
|
342
|
|
|
# -- TYPE ------------------------------------------------------------ |
343
|
|
|
self.var_type = info.get('type', 'string') |
344
|
|
|
if self.var_type not in VALID_TYPES: |
345
|
|
|
msg = 'Variable: {var_name} has an invalid type {var_type}. Valid types are: {types}' |
346
|
|
|
raise ValueError(msg.format(var_type=self.var_type, |
347
|
|
|
var_name=self.name, |
348
|
|
|
types=VALID_TYPES)) |
349
|
|
|
|
350
|
|
|
# -- SKIP_IF --------------------------------------------------------- |
351
|
|
|
self.skip_if = self.check_type('skip_if', '', str) |
352
|
|
|
|
353
|
|
|
# -- DO_IF --------------------------------------------------------- |
354
|
|
|
self.do_if = self.check_type('do_if', '', str) |
355
|
|
|
|
356
|
|
|
# -- IF_YES_SKIP_TO --------------------------------------------------------- |
357
|
|
|
self.if_yes_skip_to = self.check_type('if_yes_skip_to', None, str) |
358
|
|
|
if self.if_yes_skip_to: |
359
|
|
|
if self.var_type not in ['yes_no']: |
360
|
|
|
msg = "Variable: '{var_name}' specifies 'if_yes_skip_to' field, but variable not of type 'yes_no'" |
361
|
|
|
raise ValueError(msg.format(var_name=self.name)) |
362
|
|
|
|
363
|
|
|
# -- IF_NO_SKIP_TO --------------------------------------------------------- |
364
|
|
|
self.if_no_skip_to = self.check_type('if_no_skip_to', None, str) |
365
|
|
|
if self.if_no_skip_to: |
366
|
|
|
if self.var_type not in ['yes_no']: |
367
|
|
|
msg = "Variable: '{var_name}' specifies 'if_no_skip_to' field, but variable not of type 'yes_no'" |
368
|
|
|
raise ValueError(msg.format(var_name=self.name)) |
369
|
|
|
|
370
|
|
|
# -- PROMPT_USER ----------------------------------------------------- |
371
|
|
|
self.prompt_user = self.check_type('prompt_user', True, bool) |
372
|
|
|
# do not prompt for private variable names (beginning with _) |
373
|
|
|
if self.name.startswith('_'): |
374
|
|
|
self.prompt_user = False |
375
|
|
|
|
376
|
|
|
# -- CHOICES --------------------------------------------------------- |
377
|
|
|
# choices are somewhat special as they can be of every type |
378
|
|
|
self.choices = self.check_type('choices', [], list) |
379
|
|
|
if self.choices and default not in self.choices: |
380
|
|
|
msg = "Variable: {var_name} has an invalid default value {default} for choices: {choices}." |
381
|
|
|
raise ValueError(msg.format(var_name=self.name, default=self.default, choices=self.choices)) |
382
|
|
|
|
383
|
|
|
# -- VALIDATION STARTS ----------------------------------------------- |
384
|
|
|
self.validation = self.check_type('validation', None, str) |
385
|
|
|
|
386
|
|
|
self.validation_msg = self.check_type('validation_msg', None, str) |
387
|
|
|
|
388
|
|
|
self.validation_flag_names = self.check_type('validation_flags', [], list) |
389
|
|
|
|
390
|
|
|
self.validation_flags = 0 |
391
|
|
|
|
392
|
|
|
for vflag in self.validation_flag_names: |
393
|
|
|
if vflag in REGEX_COMPILE_FLAGS.keys(): |
394
|
|
|
self.validation_flags |= REGEX_COMPILE_FLAGS[vflag] |
395
|
|
|
else: |
396
|
|
|
msg = "Variable: {var_name} - Ignoring unkown RegEx validation Control Flag named '{flag}'\n" \ |
397
|
|
|
"Legal flag names are: {names}" |
398
|
|
|
logger.warn(msg.format(var_name=self.name, flag=vflag, |
399
|
|
|
names=REGEX_COMPILE_FLAGS.keys())) |
400
|
|
|
self.validation_flag_names.remove(vflag) |
401
|
|
|
|
402
|
|
|
self.validate = None |
403
|
|
|
if self.validation: |
404
|
|
|
try: |
405
|
|
|
self.validate = re.compile(self.validation, self.validation_flags) |
406
|
|
|
except re.error as e: |
407
|
|
|
msg = "Variable: {var_name} - Validation Setup Error: Invalid RegEx '{value}' - does not compile - {err}" |
408
|
|
|
raise ValueError(msg.format(var_name=self.name, |
409
|
|
|
value=self.validation, err=e)) |
410
|
|
|
# -- VALIDATION ENDS ------------------------------------------------- |
411
|
|
|
|
412
|
|
|
def __repr__(self): |
413
|
|
|
return "<{class_name} {variable_name}>".format( |
414
|
|
|
class_name=self.__class__.__name__, |
415
|
|
|
variable_name=self.name, |
416
|
|
|
) |
417
|
|
|
|
418
|
|
|
def __str__(self): |
419
|
|
|
s = ["{key}='{value}'".format(key=key, value=self.__dict__[key]) for key in self.__dict__ if key != 'info'] |
420
|
|
|
return self.__repr__() + ':\n' + ',\n'.join(s) |
421
|
|
|
|
422
|
|
|
def check_type(self, option_name, option_default_value, option_type): |
423
|
|
|
""" |
424
|
|
|
Retrieve the option_value named option_name from info and check its type. |
425
|
|
|
Raise ValueError if the type is incorrect; otherwise return option's value. |
426
|
|
|
""" |
427
|
|
|
option_value = self.info.get(option_name, option_default_value) |
428
|
|
|
|
429
|
|
|
if option_value is not None: |
430
|
|
|
if not isinstance(option_value, option_type): |
431
|
|
|
msg = "Variable: '{var_name}' Option: '{opt_name}' requires a value of type {type_name}, but has a value of: {value}" |
432
|
|
|
raise ValueError(msg.format(var_name=self.name, opt_name=option_name, type_name=option_type.__name__, value=option_value)) |
433
|
|
|
|
434
|
|
|
return option_value |
435
|
|
|
|
436
|
|
|
|
437
|
|
|
class CookiecutterTemplate(object): |
438
|
|
|
""" |
439
|
|
|
Embodies all attributes of a version 2 Cookiecutter template. |
440
|
|
|
""" |
441
|
|
|
|
442
|
|
|
def __init__(self, name, cookiecutter_version, variables, **info): |
443
|
|
|
""" |
444
|
|
|
Mandatorty Parameters |
445
|
|
|
|
446
|
|
|
:param name: A string, the cookiecutter template name |
447
|
|
|
:param cookiecutter_version: A string containing the version of the |
448
|
|
|
cookiecutter application that is compatible with this template. |
449
|
|
|
:param variables: A list of OrderedDict items that describe each |
450
|
|
|
variable in the template. These variables are essentially what |
451
|
|
|
is found in the version 1 cookiecutter.json file. |
452
|
|
|
|
453
|
|
|
Optional Parameters (via \**info) |
454
|
|
|
|
455
|
|
|
:param authors: An array of string - maintainers of the template. |
456
|
|
|
:param description: A string, human readable description of template. |
457
|
|
|
:param keywords: An array of string - similar to PyPI keywords. |
458
|
|
|
:param license: A string identifying the license of the template code. |
459
|
|
|
:param url: A string containing the URL for the template project. |
460
|
|
|
:param version: A string containing a version identifier, ideally |
461
|
|
|
following the semantic versioning spec. |
462
|
|
|
|
463
|
|
|
""" |
464
|
|
|
|
465
|
|
|
# mandatory fields |
466
|
|
|
self.name = name |
467
|
|
|
self.cookiecutter_version = cookiecutter_version |
468
|
|
|
self.variables = [Variable(**v) for v in variables] |
469
|
|
|
|
470
|
|
|
# optional fields |
471
|
|
|
self.authors = info.get('authors', []) |
472
|
|
|
self.description = info.get('description', None) |
473
|
|
|
self.keywords = info.get('keywords', []) |
474
|
|
|
self.license = info.get('license', None) |
475
|
|
|
self.url = info.get('url', None) |
476
|
|
|
self.version = info.get('version', None) |
477
|
|
|
|
478
|
|
|
def __repr__(self): |
479
|
|
|
return "<{class_name} {template_name}>".format( |
480
|
|
|
class_name=self.__class__.__name__, |
481
|
|
|
template_name=self.name, |
482
|
|
|
) |
483
|
|
|
|
484
|
|
|
def __iter__(self): |
485
|
|
|
for v in self.variables: |
486
|
|
|
yield v |
487
|
|
|
|
488
|
|
|
|
489
|
|
|
def load_context(json_object, no_input=False, verbose=True): |
490
|
|
|
""" |
491
|
|
|
Load a version 2 context & process the json_object for declared variables |
492
|
|
|
in the Cookiecutter template. |
493
|
|
|
|
494
|
|
|
:param json_object: A JSON file that has be loaded into a Python OrderedDict. |
495
|
|
|
:param no_input: Prompt the user at command line for manual configuration if False, |
496
|
|
|
if True, no input prompts are made, all defaults are accepted. |
497
|
|
|
:param verbose: Emit maximum varible information. |
498
|
|
|
""" |
499
|
|
|
env = Environment(extensions=['jinja2_time.TimeExtension']) |
500
|
|
|
context = collections.OrderedDict({}) |
501
|
|
|
|
502
|
|
|
skip_to_variable_name = None |
503
|
|
|
|
504
|
|
|
for variable in CookiecutterTemplate(**json_object): |
505
|
|
|
if skip_to_variable_name: |
506
|
|
|
if variable.name == skip_to_variable_name: |
507
|
|
|
skip_to_variable_name = None |
508
|
|
|
else: |
509
|
|
|
# Is executed, but not marked so in coverage report, due to |
510
|
|
|
# CPython's peephole optimizer's optimizations. |
511
|
|
|
# See https://bitbucket.org/ned/coveragepy/issues/198/continue-marked-as-not-covered |
512
|
|
|
# Issue #198 in coverage.py marked WONTFIX |
513
|
|
|
continue # pragma: no cover |
514
|
|
|
|
515
|
|
|
if variable.skip_if: |
516
|
|
|
skip_template = env.from_string(variable.skip_if) |
517
|
|
|
if skip_template.render(cookiecutter=context) == 'True': |
518
|
|
|
continue |
519
|
|
|
|
520
|
|
|
if variable.do_if: |
521
|
|
|
do_template = env.from_string(variable.do_if) |
522
|
|
|
if do_template.render(cookiecutter=context) == 'False': |
523
|
|
|
continue |
524
|
|
|
|
525
|
|
|
default = variable.default |
526
|
|
|
|
527
|
|
|
if isinstance(default, str): |
528
|
|
|
template = env.from_string(default) |
529
|
|
|
default = template.render(cookiecutter=context) |
530
|
|
|
|
531
|
|
|
deserialize = DESERIALIZERS[variable.var_type] |
532
|
|
|
|
533
|
|
|
if no_input or (not variable.prompt_user): |
534
|
|
|
context[variable.name] = deserialize(default) |
535
|
|
|
else: |
536
|
|
|
if variable.choices: |
537
|
|
|
prompt = prompt_choice |
538
|
|
|
else: |
539
|
|
|
prompt = PROMPTS[variable.var_type] |
540
|
|
|
|
541
|
|
|
if verbose and variable.description: |
542
|
|
|
click.echo(variable.description) |
543
|
|
|
|
544
|
|
|
while True: |
545
|
|
|
value = prompt(variable, default) |
546
|
|
|
if variable.validate: |
547
|
|
|
if variable.validate.match(value): |
548
|
|
|
break |
549
|
|
|
else: |
550
|
|
|
msg = "Input validation failure against regex: '{val_string}', try again!".format(val_string=variable.validation) |
551
|
|
|
click.echo(msg) |
552
|
|
|
if variable.validation_msg: |
553
|
|
|
click.echo(variable.validation_msg) |
554
|
|
|
else: |
555
|
|
|
# no validation defined |
556
|
|
|
break |
557
|
|
|
|
558
|
|
|
if verbose: |
559
|
|
|
width, _ = click.get_terminal_size() |
560
|
|
|
click.echo('-' * width) |
561
|
|
|
|
562
|
|
|
context[variable.name] = deserialize(value) |
563
|
|
|
|
564
|
|
|
if variable.if_yes_skip_to and context[variable.name] is True: |
565
|
|
|
skip_to_variable_name = variable.if_yes_skip_to |
566
|
|
|
|
567
|
|
|
if variable.if_no_skip_to and context[variable.name] is False: |
568
|
|
|
skip_to_variable_name = variable.if_no_skip_to |
569
|
|
|
|
570
|
|
|
if skip_to_variable_name: |
571
|
|
|
logger.warn("Processed all variables, but skip_to_variable_name '{}' was never found.".format(skip_to_variable_name)) |
572
|
|
|
|
573
|
|
|
return context |
574
|
|
|
|