cookiecutter.prompt.process_json()   A
last analyzed

Complexity

Conditions 4

Size

Total Lines 20
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 10
dl 0
loc 20
rs 9.9
c 0
b 0
f 0
cc 4
nop 2
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
    return click.prompt(var_name, default=default_value)
20
21
22
def read_user_yes_no(question, default_value):
23
    """Prompt the user to reply with 'yes' or 'no' (or equivalent values).
24
25
    - These input values will be converted to ``True``:
26
      "1", "true", "t", "yes", "y", "on"
27
    - These input values will be converted to ``False``:
28
      "0", "false", "f", "no", "n", "off"
29
30
    Actual parsing done by :func:`click.prompt`; Check this function codebase change in
31
    case of unexpected behaviour.
32
33
    :param str question: Question to the user
34
    :param default_value: Value that will be returned if no input happens
35
    """
36
    return click.prompt(question, default=default_value, type=click.BOOL)
37
38
39
def read_repo_password(question):
40
    """Prompt the user to enter a password.
41
42
    :param str question: Question to the user
43
    """
44
    return click.prompt(question, hide_input=True)
45
46
47
def read_user_choice(var_name, options):
48
    """Prompt the user to choose from several options for the given variable.
49
50
    The first item will be returned if no input happens.
51
52
    :param str var_name: Variable as specified in the context
53
    :param list options: Sequence of options that are available to select from
54
    :return: Exactly one item of ``options`` that has been chosen by the user
55
    """
56
    if not isinstance(options, list):
57
        raise TypeError
58
59
    if not options:
60
        raise ValueError
61
62
    choice_map = OrderedDict((f'{i}', value) for i, value in enumerate(options, 1))
63
    choices = choice_map.keys()
64
    default = '1'
65
66
    choice_lines = ['{} - {}'.format(*c) for c in choice_map.items()]
67
    prompt = '\n'.join(
68
        (
69
            f"Select {var_name}:",
70
            "\n".join(choice_lines),
71
            f"Choose from {', '.join(choices)}",
72
        )
73
    )
74
75
    user_choice = click.prompt(
76
        prompt, type=click.Choice(choices), default=default, show_choices=False
77
    )
78
    return choice_map[user_choice]
79
80
81
DEFAULT_DISPLAY = 'default'
82
83
84
def process_json(user_value, default_value=None):
85
    """Load user-supplied value as a JSON dict.
86
87
    :param str user_value: User-supplied value to load as a JSON dict
88
    """
89
    if user_value == DEFAULT_DISPLAY:
90
        # Return the given default w/o any processing
91
        return default_value
92
93
    try:
94
        user_dict = json.loads(user_value, object_pairs_hook=OrderedDict)
95
    except Exception as error:
96
        # Leave it up to click to ask the user again
97
        raise click.UsageError('Unable to decode to JSON.') from error
98
99
    if not isinstance(user_dict, dict):
100
        # Leave it up to click to ask the user again
101
        raise click.UsageError('Requires JSON dict.')
102
103
    return user_dict
104
105
106
def read_user_dict(var_name, default_value):
107
    """Prompt the user to provide a dictionary of data.
108
109
    :param str var_name: Variable as specified in the context
110
    :param default_value: Value that will be returned if no input is provided
111
    :return: A Python dictionary to use in the context.
112
    """
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
    return template.render(cookiecutter=cookiecutter_dict)
163
164
165
def prompt_choice_for_config(cookiecutter_dict, env, key, options, no_input):
166
    """Prompt user with a set of options to choose from.
167
168
    :param no_input: Do not prompt for user input and return the first available option.
169
    """
170
    rendered_options = [render_variable(env, raw, cookiecutter_dict) for raw in options]
171
    if no_input:
172
        return rendered_options[0]
173
    return read_user_choice(key, rendered_options)
174
175
176
def prompt_for_config(context, no_input=False):
177
    """Prompt user to enter a new config.
178
179
    :param dict context: Source for field names and sample values.
180
    :param no_input: Do not prompt for user input and use only values from context.
181
    """
182
    cookiecutter_dict = OrderedDict([])
183
    env = StrictEnvironment(context=context)
184
185
    # First pass: Handle simple and raw variables, plus choices.
186
    # These must be done first because the dictionaries keys and
187
    # values might refer to them.
188
    for key, raw in context['cookiecutter'].items():
189
        if key.startswith('_') and not key.startswith('__'):
190
            cookiecutter_dict[key] = raw
191
            continue
192
        elif key.startswith('__'):
193
            cookiecutter_dict[key] = render_variable(env, raw, cookiecutter_dict)
194
            continue
195
196
        try:
197
            if isinstance(raw, list):
198
                # We are dealing with a choice variable
199
                val = prompt_choice_for_config(
200
                    cookiecutter_dict, env, key, raw, no_input
201
                )
202
                cookiecutter_dict[key] = val
203
            elif isinstance(raw, bool):
204
                # We are dealing with a boolean variable
205
                if no_input:
206
                    cookiecutter_dict[key] = render_variable(
207
                        env, raw, cookiecutter_dict
208
                    )
209
                else:
210
                    cookiecutter_dict[key] = read_user_yes_no(key, raw)
211
            elif not isinstance(raw, dict):
212
                # We are dealing with a regular variable
213
                val = render_variable(env, raw, cookiecutter_dict)
214
215
                if not no_input:
216
                    val = read_user_variable(key, val)
217
218
                cookiecutter_dict[key] = val
219
        except UndefinedError as err:
220
            msg = f"Unable to render variable '{key}'"
221
            raise UndefinedVariableInTemplate(msg, err, context) from err
222
223
    # Second pass; handle the dictionaries.
224
    for key, raw in context['cookiecutter'].items():
225
        # Skip private type dicts not to be rendered.
226
        if key.startswith('_') and not key.startswith('__'):
227
            continue
228
229
        try:
230
            if isinstance(raw, dict):
231
                # We are dealing with a dict variable
232
                val = render_variable(env, raw, cookiecutter_dict)
233
234
                if not no_input and not key.startswith('__'):
235
                    val = read_user_dict(key, val)
236
237
                cookiecutter_dict[key] = val
238
        except UndefinedError as err:
239
            msg = f"Unable to render variable '{key}'"
240
            raise UndefinedVariableInTemplate(msg, err, context) from err
241
242
    return cookiecutter_dict
243