from typing import Any from typing import Dict from typing import Iterable from typing import Mapping from typing import Optional from typing import Union from prompt_toolkit.output import ColorDepth from questionary import utils from questionary.constants import DEFAULT_KBI_MESSAGE from questionary.prompts import AVAILABLE_PROMPTS from questionary.prompts import prompt_by_name from questionary.prompts.common import print_formatted_text class PromptParameterException(ValueError): """Received a prompt with a missing parameter.""" def __init__(self, message: str, errors: Optional[BaseException] = None) -> None: # Call the base class constructor with the parameters it needs super().__init__(f"You must provide a `{message}` value", errors) def prompt( questions: Union[Dict[str, Any], Iterable[Mapping[str, Any]]], answers: Optional[Mapping[str, Any]] = None, patch_stdout: bool = False, true_color: bool = False, kbi_msg: str = DEFAULT_KBI_MESSAGE, **kwargs: Any, ) -> Dict[str, Any]: """Prompt the user for input on all the questions. Catches keyboard interrupts and prints a message. See :func:`unsafe_prompt` for possible question configurations. Args: questions: A list of question configs representing questions to ask. A question config may have the following options: * type - The type of question. * name - An ID for the question (to identify it in the answers :obj:`dict`). * when - Callable to conditionally show the question. This function takes a :obj:`dict` representing the current answers. * filter - Function that the answer is passed to. The return value of this function is saved as the answer. Additional options correspond to the parameter names for particular question types. answers: Default answers. patch_stdout: Ensure that the prompt renders correctly if other threads are printing to stdout. kbi_msg: The message to be printed on a keyboard interrupt. true_color: Use true color output. color_depth: Color depth to use. If ``true_color`` is set to true then this value is ignored. type: Default ``type`` value to use in question config. filter: Default ``filter`` value to use in question config. name: Default ``name`` value to use in question config. when: Default ``when`` value to use in question config. default: Default ``default`` value to use in question config. kwargs: Additional options passed to every question. Returns: Dictionary of question answers. """ try: return unsafe_prompt(questions, answers, patch_stdout, true_color, **kwargs) except KeyboardInterrupt: print("") print(kbi_msg) print("") return {} def unsafe_prompt( questions: Union[Dict[str, Any], Iterable[Mapping[str, Any]]], answers: Optional[Mapping[str, Any]] = None, patch_stdout: bool = False, true_color: bool = False, **kwargs: Any, ) -> Dict[str, Any]: """Prompt the user for input on all the questions. Won't catch keyboard interrupts. Args: questions: A list of question configs representing questions to ask. A question config may have the following options: * type - The type of question. * name - An ID for the question (to identify it in the answers :obj:`dict`). * when - Callable to conditionally show the question. This function takes a :obj:`dict` representing the current answers. * filter - Function that the answer is passed to. The return value of this function is saved as the answer. Additional options correspond to the parameter names for particular question types. answers: Default answers. patch_stdout: Ensure that the prompt renders correctly if other threads are printing to stdout. true_color: Use true color output. color_depth: Color depth to use. If ``true_color`` is set to true then this value is ignored. type: Default ``type`` value to use in question config. filter: Default ``filter`` value to use in question config. name: Default ``name`` value to use in question config. when: Default ``when`` value to use in question config. default: Default ``default`` value to use in question config. kwargs: Additional options passed to every question. Returns: Dictionary of question answers. Raises: KeyboardInterrupt: raised on keyboard interrupt """ if isinstance(questions, dict): questions = [questions] answers = dict(answers or {}) for question_config in questions: question_config = dict(question_config) # import the question if "type" not in question_config: raise PromptParameterException("type") # every type except 'print' needs a name if "name" not in question_config and question_config["type"] != "print": raise PromptParameterException("name") _kwargs = kwargs.copy() _kwargs.update(question_config) _type = _kwargs.pop("type") _filter = _kwargs.pop("filter", None) name = _kwargs.pop("name", None) if _type == "print" else _kwargs.pop("name") when = _kwargs.pop("when", None) if true_color: _kwargs["color_depth"] = ColorDepth.TRUE_COLOR if when: # at least a little sanity check! if callable(question_config["when"]): try: if not question_config["when"](answers): continue except Exception as exception: raise ValueError( f"Problem in 'when' check of " f"{name} question: {exception}" ) from exception else: raise ValueError( "'when' needs to be function that accepts a dict argument" ) # handle 'print' type if _type == "print": try: message = _kwargs.pop("message") except KeyError as e: raise PromptParameterException("message") from e # questions can take 'input' arg but print_formatted_text does not # Remove 'input', if present, to avoid breaking during tests _kwargs.pop("input", None) print_formatted_text(message, **_kwargs) if name: answers[name] = None continue choices = question_config.get("choices") if choices is not None and callable(choices): calculated_choices = choices(answers) question_config["choices"] = calculated_choices kwargs["choices"] = calculated_choices if _filter: # at least a little sanity check! if not callable(_filter): raise ValueError( "'filter' needs to be function that accepts an argument" ) if callable(question_config.get("default")): _kwargs["default"] = question_config["default"](answers) create_question_func = prompt_by_name(_type) if not create_question_func: raise ValueError( f"No question type '{_type}' found. " f"Known question types are {', '.join(AVAILABLE_PROMPTS)}." ) missing_args = list(utils.missing_arguments(create_question_func, _kwargs)) if missing_args: raise PromptParameterException(missing_args[0]) question = create_question_func(**_kwargs) answer = question.unsafe_ask(patch_stdout) if answer is not None: if _filter: try: answer = _filter(answer) except Exception as exception: raise ValueError( f"Problem processing 'filter' of {name} " f"question: {exception}" ) from exception answers[name] = answer return answers