forked from cookiecutter/cookiecutter
/
context.py
272 lines (204 loc) · 7.16 KB
/
context.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
# -*- coding: utf-8 -*-
import codecs
import collections
import json
import pprint
import click
from jinja2 import Environment
DEFAULT_PROMPT = 'Please enter a value for "{variable.name}"'
VALID_TYPES = [
'boolean',
'yes_no',
'int',
'json',
'string',
]
def prompt_string(variable, default):
return click.prompt(
variable.prompt,
default=default,
hide_input=variable.hide_input,
type=click.STRING,
)
def prompt_boolean(variable, default):
return click.prompt(
variable.prompt,
default=default,
hide_input=variable.hide_input,
type=click.BOOL,
)
def prompt_int(variable, default):
return click.prompt(
variable.prompt,
default=default,
hide_input=variable.hide_input,
type=click.INT,
)
def prompt_json(variable, default):
# The JSON object from cookiecutter.json might be very large
# We only show 'default'
DEFAULT_JSON = 'default'
def process_json(user_value):
try:
return json.loads(
user_value,
object_pairs_hook=collections.OrderedDict,
)
except json.decoder.JSONDecodeError:
# Leave it up to click to ask the user again
raise click.UsageError('Unable to decode to JSON.')
dict_value = click.prompt(
variable.prompt,
default=DEFAULT_JSON,
hide_input=variable.hide_input,
type=click.STRING,
value_proc=process_json,
)
if dict_value == DEFAULT_JSON:
# Return the given default w/o any processing
return default
return dict_value
def prompt_yes_no(variable, default):
if default is True:
default_display = 'y'
else:
default_display = 'n'
return click.prompt(
variable.prompt,
default=default_display,
hide_input=variable.hide_input,
type=click.BOOL,
)
def prompt_choice(variable, default):
"""Returns prompt, default and callback for a choice variable"""
choice_map = collections.OrderedDict(
(u'{}'.format(i), value)
for i, value in enumerate(variable.choices, 1)
)
choices = choice_map.keys()
prompt = u'\n'.join((
variable.prompt,
u'\n'.join([u'{} - {}'.format(*c) for c in choice_map.items()]),
u'Choose from {}'.format(u', '.join(choices)),
))
default = str(variable.choices.index(default) + 1)
user_choice = click.prompt(
prompt,
default=default,
hide_input=variable.hide_input,
type=click.Choice(choices),
)
return choice_map[user_choice]
PROMPTS = {
'string': prompt_string,
'boolean': prompt_boolean,
'int': prompt_int,
'json': prompt_json,
'yes_no': prompt_yes_no,
}
def deserialize_string(value):
return str(value)
def deserialize_boolean(value):
return bool(value)
def deserialize_yes_no(value):
return bool(value)
def deserialize_int(value):
return int(value)
def deserialize_json(value):
return value
DESERIALIZERS = {
'string': deserialize_string,
'boolean': deserialize_boolean,
'int': deserialize_int,
'json': deserialize_json,
'yes_no': deserialize_yes_no,
}
class Variable(object):
def __init__(self, name, default, **info):
# mandatory fields
self.name = name
self.default = default
# optional fields
self.description = info.get('description', None)
self.prompt = info.get('prompt', DEFAULT_PROMPT.format(variable=self))
self.hide_input = info.get('hide_input', False)
self.var_type = info.get('type', 'string')
if self.var_type not in VALID_TYPES:
msg = 'Invalid type {var_type} for variable'
raise ValueError(msg.format(var_type=self.var_type))
self.skip_if = info.get('skip_if', '')
if not isinstance(self.skip_if, str):
# skip_if was specified in cookiecutter.json
msg = 'Field skip_if is required to be a str, got {value}'
raise ValueError(msg.format(value=self.skip_if))
self.prompt_user = info.get('prompt_user', True)
if not isinstance(self.prompt_user, bool):
# prompt_user was specified in cookiecutter.json
msg = 'Field prompt_user is required to be a bool, got {value}'
raise ValueError(msg.format(value=self.prompt_user))
# choices are somewhat special as they can of every type
self.choices = info.get('choices', [])
if self.choices and default not in self.choices:
msg = 'Invalid default value {default} for choice variable'
raise ValueError(msg.format(default=self.default))
def __repr__(self):
return "<{class_name} {variable_name}>".format(
class_name=self.__class__.__name__,
variable_name=self.name,
)
class CookiecutterTemplate(object):
def __init__(self, name, cookiecutter_version, variables, **info):
# mandatory fields
self.name = name
self.cookiecutter_version = cookiecutter_version
self.variables = [Variable(**v) for v in variables]
# optional fields
self.authors = info.get('authors', [])
self.description = info.get('description', None)
self.keywords = info.get('keywords', [])
self.license = info.get('license', None)
self.url = info.get('url', None)
self.version = info.get('version', None)
def __repr__(self):
return "<{class_name} {template_name}>".format(
class_name=self.__class__.__name__,
template_name=self.name,
)
def __iter__(self):
for v in self.variables:
yield v
def load_context(json_object, verbose):
env = Environment(extensions=['jinja2_time.TimeExtension'])
context = collections.OrderedDict({})
for variable in CookiecutterTemplate(**json_object):
if variable.skip_if:
skip_template = env.from_string(variable.skip_if)
if skip_template.render(cookiecutter=context) == 'True':
continue
default = variable.default
if isinstance(default, str):
template = env.from_string(default)
default = template.render(cookiecutter=context)
deserialize = DESERIALIZERS[variable.var_type]
if not variable.prompt_user:
context[variable.name] = deserialize(default)
continue
if variable.choices:
prompt = prompt_choice
else:
prompt = PROMPTS[variable.var_type]
if verbose and variable.description:
click.echo(variable.description)
value = prompt(variable, default)
if verbose:
width, _ = click.get_terminal_size()
click.echo('-' * width)
context[variable.name] = deserialize(value)
return context
def main(file_path):
"""Load the json object and prompt the user for input"""
with codecs.open(file_path, 'r', encoding='utf8') as f:
json_object = json.load(f, object_pairs_hook=collections.OrderedDict)
pprint.pprint(load_context(json_object, True))
if __name__ == '__main__':
main('tests/new-context/cookiecutter.json')