diff options
Diffstat (limited to '')
-rw-r--r-- | third_party/python/cookiecutter/cookiecutter/prompt.py | 236 |
1 files changed, 236 insertions, 0 deletions
diff --git a/third_party/python/cookiecutter/cookiecutter/prompt.py b/third_party/python/cookiecutter/cookiecutter/prompt.py new file mode 100644 index 0000000000..f06cdc3c0b --- /dev/null +++ b/third_party/python/cookiecutter/cookiecutter/prompt.py @@ -0,0 +1,236 @@ +"""Functions for prompting the user for project info.""" +import functools +import json +from collections import OrderedDict + +import click +from jinja2.exceptions import UndefinedError + +from cookiecutter.environment import StrictEnvironment +from cookiecutter.exceptions import UndefinedVariableInTemplate + + +def read_user_variable(var_name, default_value): + """Prompt user for variable and return the entered value or given default. + + :param str var_name: Variable of the context to query the user + :param default_value: Value that will be returned if no input happens + """ + # Please see https://click.palletsprojects.com/en/7.x/api/#click.prompt + return click.prompt(var_name, default=default_value) + + +def read_user_yes_no(question, default_value): + """Prompt the user to reply with 'yes' or 'no' (or equivalent values). + + Note: + Possible choices are 'true', '1', 'yes', 'y' or 'false', '0', 'no', 'n' + + :param str question: Question to the user + :param default_value: Value that will be returned if no input happens + """ + # Please see https://click.palletsprojects.com/en/7.x/api/#click.prompt + return click.prompt(question, default=default_value, type=click.BOOL) + + +def read_repo_password(question): + """Prompt the user to enter a password. + + :param str question: Question to the user + """ + # Please see https://click.palletsprojects.com/en/7.x/api/#click.prompt + return click.prompt(question, hide_input=True) + + +def read_user_choice(var_name, options): + """Prompt the user to choose from several options for the given variable. + + The first item will be returned if no input happens. + + :param str var_name: Variable as specified in the context + :param list options: Sequence of options that are available to select from + :return: Exactly one item of ``options`` that has been chosen by the user + """ + # Please see https://click.palletsprojects.com/en/7.x/api/#click.prompt + if not isinstance(options, list): + raise TypeError + + if not options: + raise ValueError + + choice_map = OrderedDict((f'{i}', value) for i, value in enumerate(options, 1)) + choices = choice_map.keys() + default = '1' + + choice_lines = ['{} - {}'.format(*c) for c in choice_map.items()] + prompt = '\n'.join( + ( + f'Select {var_name}:', + '\n'.join(choice_lines), + 'Choose from {}'.format(', '.join(choices)), + ) + ) + + user_choice = click.prompt( + prompt, type=click.Choice(choices), default=default, show_choices=False + ) + return choice_map[user_choice] + + +DEFAULT_DISPLAY = 'default' + + +def process_json(user_value, default_value=None): + """Load user-supplied value as a JSON dict. + + :param str user_value: User-supplied value to load as a JSON dict + """ + if user_value == DEFAULT_DISPLAY: + # Return the given default w/o any processing + return default_value + + try: + user_dict = json.loads(user_value, object_pairs_hook=OrderedDict) + except Exception: + # Leave it up to click to ask the user again + raise click.UsageError('Unable to decode to JSON.') + + if not isinstance(user_dict, dict): + # Leave it up to click to ask the user again + raise click.UsageError('Requires JSON dict.') + + return user_dict + + +def read_user_dict(var_name, default_value): + """Prompt the user to provide a dictionary of data. + + :param str var_name: Variable as specified in the context + :param default_value: Value that will be returned if no input is provided + :return: A Python dictionary to use in the context. + """ + # Please see https://click.palletsprojects.com/en/7.x/api/#click.prompt + if not isinstance(default_value, dict): + raise TypeError + + user_value = click.prompt( + var_name, + default=DEFAULT_DISPLAY, + type=click.STRING, + value_proc=functools.partial(process_json, default_value=default_value), + ) + + if click.__version__.startswith("7.") and user_value == DEFAULT_DISPLAY: + # click 7.x does not invoke value_proc on the default value. + return default_value # pragma: no cover + return user_value + + +def render_variable(env, raw, cookiecutter_dict): + """Render the next variable to be displayed in the user prompt. + + Inside the prompting taken from the cookiecutter.json file, this renders + the next variable. For example, if a project_name is "Peanut Butter + Cookie", the repo_name could be be rendered with: + + `{{ cookiecutter.project_name.replace(" ", "_") }}`. + + This is then presented to the user as the default. + + :param Environment env: A Jinja2 Environment object. + :param raw: The next value to be prompted for by the user. + :param dict cookiecutter_dict: The current context as it's gradually + being populated with variables. + :return: The rendered value for the default variable. + """ + if raw is None: + return None + elif isinstance(raw, dict): + return { + render_variable(env, k, cookiecutter_dict): render_variable( + env, v, cookiecutter_dict + ) + for k, v in raw.items() + } + elif isinstance(raw, list): + return [render_variable(env, v, cookiecutter_dict) for v in raw] + elif not isinstance(raw, str): + raw = str(raw) + + template = env.from_string(raw) + + rendered_template = template.render(cookiecutter=cookiecutter_dict) + return rendered_template + + +def prompt_choice_for_config(cookiecutter_dict, env, key, options, no_input): + """Prompt user with a set of options to choose from. + + Each of the possible choices is rendered beforehand. + """ + rendered_options = [render_variable(env, raw, cookiecutter_dict) for raw in options] + + if no_input: + return rendered_options[0] + return read_user_choice(key, rendered_options) + + +def prompt_for_config(context, no_input=False): + """Prompt user to enter a new config. + + :param dict context: Source for field names and sample values. + :param no_input: Prompt the user at command line for manual configuration? + """ + cookiecutter_dict = OrderedDict([]) + env = StrictEnvironment(context=context) + + # First pass: Handle simple and raw variables, plus choices. + # These must be done first because the dictionaries keys and + # values might refer to them. + for key, raw in context['cookiecutter'].items(): + if key.startswith('_') and not key.startswith('__'): + cookiecutter_dict[key] = raw + continue + elif key.startswith('__'): + cookiecutter_dict[key] = render_variable(env, raw, cookiecutter_dict) + continue + + try: + if isinstance(raw, list): + # We are dealing with a choice variable + val = prompt_choice_for_config( + cookiecutter_dict, env, key, raw, no_input + ) + cookiecutter_dict[key] = val + elif not isinstance(raw, dict): + # We are dealing with a regular variable + val = render_variable(env, raw, cookiecutter_dict) + + if not no_input: + val = read_user_variable(key, val) + + cookiecutter_dict[key] = val + except UndefinedError as err: + msg = f"Unable to render variable '{key}'" + raise UndefinedVariableInTemplate(msg, err, context) + + # Second pass; handle the dictionaries. + for key, raw in context['cookiecutter'].items(): + # Skip private type dicts not ot be rendered. + if key.startswith('_') and not key.startswith('__'): + continue + + try: + if isinstance(raw, dict): + # We are dealing with a dict variable + val = render_variable(env, raw, cookiecutter_dict) + + if not no_input and not key.startswith('__'): + val = read_user_dict(key, val) + + cookiecutter_dict[key] = val + except UndefinedError as err: + msg = f"Unable to render variable '{key}'" + raise UndefinedVariableInTemplate(msg, err, context) + + return cookiecutter_dict |