cookiecutter.prompt   A
last analyzed

Complexity

Total Complexity 30

Size/Duplication

Total Lines 241
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 30
eloc 111
dl 0
loc 241
rs 10
c 0
b 0
f 0

9 Functions

Rating   Name   Duplication   Size   Complexity  
A read_user_choice() 0 33 3
A render_variable() 0 37 5
A read_repo_password() 0 7 1
C prompt_for_config() 0 53 11
A read_user_variable() 0 8 1
A prompt_choice_for_config() 0 12 2
A process_json() 0 19 3
A read_user_yes_no() 0 14 1
A read_user_dict() 0 24 3
1
# -*- coding: utf-8 -*-
2
3
"""Functions for prompting the user for project info."""
4
5
from collections import OrderedDict
6
import json
7
8
import click
9
import six
10
11
from jinja2.exceptions import UndefinedError
12
13
from cookiecutter.exceptions import UndefinedVariableInTemplate
14
from cookiecutter.environment import StrictEnvironment
15
16
17
def read_user_variable(var_name, default_value):
18
    """Prompt user for variable and return the entered value or given default.
19
20
    :param str var_name: Variable of the context to query the user
21
    :param default_value: Value that will be returned if no input happens
22
    """
23
    # Please see http://click.pocoo.org/4/api/#click.prompt
24
    return click.prompt(var_name, default=default_value)
25
26
27
def read_user_yes_no(question, default_value):
28
    """Prompt the user to reply with 'yes' or 'no' (or equivalent values).
29
30
    Note:
31
      Possible choices are 'true', '1', 'yes', 'y' or 'false', '0', 'no', 'n'
32
33
    :param str question: Question to the user
34
    :param default_value: Value that will be returned if no input happens
35
    """
36
    # Please see http://click.pocoo.org/4/api/#click.prompt
37
    return click.prompt(
38
        question,
39
        default=default_value,
40
        type=click.BOOL
41
    )
42
43
44
def read_repo_password(question):
45
    """Prompt the user to enter a password.
46
47
    :param str question: Question to the user
48
    """
49
    # Please see http://click.pocoo.org/4/api/#click.prompt
50
    return click.prompt(question, hide_input=True)
51
52
53
def read_user_choice(var_name, options):
54
    """Prompt the user to choose from several options for the given variable.
55
56
    The first item will be returned if no input happens.
57
58
    :param str var_name: Variable as specified in the context
59
    :param list options: Sequence of options that are available to select from
60
    :return: Exactly one item of ``options`` that has been chosen by the user
61
    """
62
    # Please see http://click.pocoo.org/4/api/#click.prompt
63
    if not isinstance(options, list):
64
        raise TypeError
65
66
    if not options:
67
        raise ValueError
68
69
    choice_map = OrderedDict(
70
        (u'{}'.format(i), value) for i, value in enumerate(options, 1)
71
    )
72
    choices = choice_map.keys()
73
    default = u'1'
74
75
    choice_lines = [u'{} - {}'.format(*c) for c in choice_map.items()]
76
    prompt = u'\n'.join((
77
        u'Select {}:'.format(var_name),
78
        u'\n'.join(choice_lines),
79
        u'Choose from {}'.format(u', '.join(choices))
80
    ))
81
82
    user_choice = click.prompt(
83
        prompt, type=click.Choice(choices), default=default, show_choices=False
84
    )
85
    return choice_map[user_choice]
86
87
88
def process_json(user_value):
89
    """Load user-supplied value as a JSON dict.
90
91
    :param str user_value: User-supplied value to load as a JSON dict
92
    """
93
    try:
94
        user_dict = json.loads(
95
            user_value,
96
            object_pairs_hook=OrderedDict,
97
        )
98
    except Exception:
99
        # Leave it up to click to ask the user again
100
        raise click.UsageError('Unable to decode to JSON.')
101
102
    if not isinstance(user_dict, dict):
103
        # Leave it up to click to ask the user again
104
        raise click.UsageError('Requires JSON dict.')
105
106
    return user_dict
107
108
109
def read_user_dict(var_name, default_value):
110
    """Prompt the user to provide a dictionary of data.
111
112
    :param str var_name: Variable as specified in the context
113
    :param default_value: Value that will be returned if no input is provided
114
    :return: A Python dictionary to use in the context.
115
    """
116
    # Please see http://click.pocoo.org/4/api/#click.prompt
117
    if not isinstance(default_value, dict):
118
        raise TypeError
119
120
    default_display = 'default'
121
122
    user_value = click.prompt(
123
        var_name,
124
        default=default_display,
125
        type=click.STRING,
126
        value_proc=process_json,
127
    )
128
129
    if user_value == default_display:
130
        # Return the given default w/o any processing
131
        return default_value
132
    return user_value
133
134
135
def render_variable(env, raw, cookiecutter_dict):
136
    """Render the next variable to be displayed in the user prompt.
137
138
    Inside the prompting taken from the cookiecutter.json file, this renders
139
    the next variable. For example, if a project_name is "Peanut Butter
140
    Cookie", the repo_name could be be rendered with:
141
142
        `{{ cookiecutter.project_name.replace(" ", "_") }}`.
143
144
    This is then presented to the user as the default.
145
146
    :param Environment env: A Jinja2 Environment object.
147
    :param str raw: The next value to be prompted for by the user.
148
    :param dict cookiecutter_dict: The current context as it's gradually
149
        being populated with variables.
150
    :return: The rendered value for the default variable.
151
    """
152
    if raw is None:
153
        return None
154
    elif isinstance(raw, dict):
155
        return {
156
            render_variable(env, k, cookiecutter_dict):
157
                render_variable(env, v, cookiecutter_dict)
158
            for k, v in raw.items()
159
        }
160
    elif isinstance(raw, list):
161
        return [
162
            render_variable(env, v, cookiecutter_dict)
163
            for v in raw
164
        ]
165
    elif not isinstance(raw, six.string_types):
166
        raw = str(raw)
167
168
    template = env.from_string(raw)
169
170
    rendered_template = template.render(cookiecutter=cookiecutter_dict)
171
    return rendered_template
172
173
174
def prompt_choice_for_config(cookiecutter_dict, env, key, options, no_input):
175
    """Prompt user with a set of options to choose from.
176
177
    Each of the possible choices is rendered beforehand.
178
    """
179
    rendered_options = [
180
        render_variable(env, raw, cookiecutter_dict) for raw in options
181
    ]
182
183
    if no_input:
184
        return rendered_options[0]
185
    return read_user_choice(key, rendered_options)
186
187
188
def prompt_for_config(context, no_input=False):
189
    """Prompt user to enter a new config.
190
191
    :param dict context: Source for field names and sample values.
192
    :param no_input: Prompt the user at command line for manual configuration?
193
    """
194
    cookiecutter_dict = OrderedDict([])
195
    env = StrictEnvironment(context=context)
196
197
    # First pass: Handle simple and raw variables, plus choices.
198
    # These must be done first because the dictionaries keys and
199
    # values might refer to them.
200
    for key, raw in context[u'cookiecutter'].items():
201
        if key.startswith(u'_'):
202
            cookiecutter_dict[key] = raw
203
            continue
204
205
        try:
206
            if isinstance(raw, list):
207
                # We are dealing with a choice variable
208
                val = prompt_choice_for_config(
209
                    cookiecutter_dict, env, key, raw, no_input
210
                )
211
                cookiecutter_dict[key] = val
212
            elif not isinstance(raw, dict):
213
                # We are dealing with a regular variable
214
                val = render_variable(env, raw, cookiecutter_dict)
215
216
                if not no_input:
217
                    val = read_user_variable(key, val)
218
219
                cookiecutter_dict[key] = val
220
        except UndefinedError as err:
221
            msg = "Unable to render variable '{}'".format(key)
222
            raise UndefinedVariableInTemplate(msg, err, context)
223
224
    # Second pass; handle the dictionaries.
225
    for key, raw in context[u'cookiecutter'].items():
226
227
        try:
228
            if isinstance(raw, dict):
229
                # We are dealing with a dict variable
230
                val = render_variable(env, raw, cookiecutter_dict)
231
232
                if not no_input:
233
                    val = read_user_dict(key, val)
234
235
                cookiecutter_dict[key] = val
236
        except UndefinedError as err:
237
            msg = "Unable to render variable '{}'".format(key)
238
            raise UndefinedVariableInTemplate(msg, err, context)
239
240
    return cookiecutter_dict
241