Passed
Push — master ( 47312b...0d4b48 )
by Andrey
01:13
created

cookiecutter.prompt.prompt_for_config()   C

Complexity

Conditions 11

Size

Total Lines 53
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 11
eloc 31
nop 2
dl 0
loc 53
rs 5.4
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like cookiecutter.prompt.prompt_for_config() often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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 https://click.palletsprojects.com/en/7.x/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 https://click.palletsprojects.com/en/7.x/api/#click.prompt
37
    return click.prompt(question, default=default_value, type=click.BOOL)
38
39
40
def read_repo_password(question):
41
    """Prompt the user to enter a password.
42
43
    :param str question: Question to the user
44
    """
45
    # Please see https://click.palletsprojects.com/en/7.x/api/#click.prompt
46
    return click.prompt(question, hide_input=True)
47
48
49
def read_user_choice(var_name, options):
50
    """Prompt the user to choose from several options for the given variable.
51
52
    The first item will be returned if no input happens.
53
54
    :param str var_name: Variable as specified in the context
55
    :param list options: Sequence of options that are available to select from
56
    :return: Exactly one item of ``options`` that has been chosen by the user
57
    """
58
    # Please see https://click.palletsprojects.com/en/7.x/api/#click.prompt
59
    if not isinstance(options, list):
60
        raise TypeError
61
62
    if not options:
63
        raise ValueError
64
65
    choice_map = OrderedDict(
66
        (u'{}'.format(i), value) for i, value in enumerate(options, 1)
67
    )
68
    choices = choice_map.keys()
69
    default = u'1'
70
71
    choice_lines = [u'{} - {}'.format(*c) for c in choice_map.items()]
72
    prompt = u'\n'.join(
73
        (
74
            u'Select {}:'.format(var_name),
75
            u'\n'.join(choice_lines),
76
            u'Choose from {}'.format(u', '.join(choices)),
77
        )
78
    )
79
80
    user_choice = click.prompt(
81
        prompt, type=click.Choice(choices), default=default, show_choices=False
82
    )
83
    return choice_map[user_choice]
84
85
86
def process_json(user_value):
87
    """Load user-supplied value as a JSON dict.
88
89
    :param str user_value: User-supplied value to load as a JSON dict
90
    """
91
    try:
92
        user_dict = json.loads(user_value, object_pairs_hook=OrderedDict)
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 https://click.palletsprojects.com/en/7.x/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, default=default_display, type=click.STRING, value_proc=process_json
119
    )
120
121
    if user_value == default_display:
122
        # Return the given default w/o any processing
123
        return default_value
124
    return user_value
125
126
127
def render_variable(env, raw, cookiecutter_dict):
128
    """Render the next variable to be displayed in the user prompt.
129
130
    Inside the prompting taken from the cookiecutter.json file, this renders
131
    the next variable. For example, if a project_name is "Peanut Butter
132
    Cookie", the repo_name could be be rendered with:
133
134
        `{{ cookiecutter.project_name.replace(" ", "_") }}`.
135
136
    This is then presented to the user as the default.
137
138
    :param Environment env: A Jinja2 Environment object.
139
    :param raw: The next value to be prompted for by the user.
140
    :param dict cookiecutter_dict: The current context as it's gradually
141
        being populated with variables.
142
    :return: The rendered value for the default variable.
143
    """
144
    if raw is None:
145
        return None
146
    elif isinstance(raw, dict):
147
        return {
148
            render_variable(env, k, cookiecutter_dict): render_variable(
149
                env, v, cookiecutter_dict
150
            )
151
            for k, v in raw.items()
152
        }
153
    elif isinstance(raw, list):
154
        return [render_variable(env, v, cookiecutter_dict) for v in raw]
155
    elif not isinstance(raw, six.string_types):
156
        raw = str(raw)
157
158
    template = env.from_string(raw)
159
160
    rendered_template = template.render(cookiecutter=cookiecutter_dict)
161
    return rendered_template
162
163
164
def prompt_choice_for_config(cookiecutter_dict, env, key, options, no_input):
165
    """Prompt user with a set of options to choose from.
166
167
    Each of the possible choices is rendered beforehand.
168
    """
169
    rendered_options = [render_variable(env, raw, cookiecutter_dict) for raw in options]
170
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: Prompt the user at command line for manual configuration?
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[u'cookiecutter'].items():
189
        if key.startswith(u'_'):
190
            cookiecutter_dict[key] = raw
191
            continue
192
193
        try:
194
            if isinstance(raw, list):
195
                # We are dealing with a choice variable
196
                val = prompt_choice_for_config(
197
                    cookiecutter_dict, env, key, raw, no_input
198
                )
199
                cookiecutter_dict[key] = val
200
            elif not isinstance(raw, dict):
201
                # We are dealing with a regular variable
202
                val = render_variable(env, raw, cookiecutter_dict)
203
204
                if not no_input:
205
                    val = read_user_variable(key, val)
206
207
                cookiecutter_dict[key] = val
208
        except UndefinedError as err:
209
            msg = "Unable to render variable '{}'".format(key)
210
            raise UndefinedVariableInTemplate(msg, err, context)
211
212
    # Second pass; handle the dictionaries.
213
    for key, raw in context[u'cookiecutter'].items():
214
215
        try:
216
            if isinstance(raw, dict):
217
                # We are dealing with a dict variable
218
                val = render_variable(env, raw, cookiecutter_dict)
219
220
                if not no_input:
221
                    val = read_user_dict(key, val)
222
223
                cookiecutter_dict[key] = val
224
        except UndefinedError as err:
225
            msg = "Unable to render variable '{}'".format(key)
226
            raise UndefinedVariableInTemplate(msg, err, context)
227
228
    return cookiecutter_dict
229