Completed
Push — master ( 01b6e4...5ea952 )
by Andrey
33s queued 13s
created

cookiecutter.prompt.render_variable()   B

Complexity

Conditions 6

Size

Total Lines 35
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

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