Completed
Push — master ( 40649d...34b7b7 )
by
unknown
9s
created

read_user_dict()   B

Complexity

Conditions 3

Size

Total Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

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