From 040e9d5ec0102c5fd421b95a5e804275e8792ce6 Mon Sep 17 00:00:00 2001
From: Weiduhuo <1281586535@qq.com>
Date: Sun, 29 Oct 2023 15:19:41 +0800
Subject: [PATCH] add questionary module from 'tmbo/questionary/pull/330'
---
requirements.txt | 1 +
utils/questionary/NOTICE | 51 ++
utils/questionary/README.md | 114 ++++
utils/questionary/questionary/__init__.py | 54 ++
utils/questionary/questionary/constants.py | 49 ++
utils/questionary/questionary/form.py | 122 ++++
utils/questionary/questionary/prompt.py | 236 +++++++
.../questionary/prompts/__init__.py | 29 +
.../questionary/prompts/autocomplete.py | 214 +++++++
.../questionary/prompts/checkbox.py | 300 +++++++++
.../questionary/questionary/prompts/common.py | 579 ++++++++++++++++++
.../questionary/prompts/confirm.py | 133 ++++
.../questionary/prompts/password.py | 61 ++
utils/questionary/questionary/prompts/path.py | 243 ++++++++
.../prompts/press_any_key_to_continue.py | 61 ++
.../questionary/prompts/rawselect.py | 79 +++
.../questionary/questionary/prompts/select.py | 258 ++++++++
utils/questionary/questionary/prompts/text.py | 101 +++
utils/questionary/questionary/py.typed | 0
utils/questionary/questionary/question.py | 134 ++++
utils/questionary/questionary/styles.py | 16 +
utils/questionary/questionary/utils.py | 78 +++
utils/questionary/questionary/version.py | 1 +
23 files changed, 2914 insertions(+)
create mode 100644 utils/questionary/NOTICE
create mode 100644 utils/questionary/README.md
create mode 100644 utils/questionary/questionary/__init__.py
create mode 100644 utils/questionary/questionary/constants.py
create mode 100644 utils/questionary/questionary/form.py
create mode 100644 utils/questionary/questionary/prompt.py
create mode 100644 utils/questionary/questionary/prompts/__init__.py
create mode 100644 utils/questionary/questionary/prompts/autocomplete.py
create mode 100644 utils/questionary/questionary/prompts/checkbox.py
create mode 100644 utils/questionary/questionary/prompts/common.py
create mode 100644 utils/questionary/questionary/prompts/confirm.py
create mode 100644 utils/questionary/questionary/prompts/password.py
create mode 100644 utils/questionary/questionary/prompts/path.py
create mode 100644 utils/questionary/questionary/prompts/press_any_key_to_continue.py
create mode 100644 utils/questionary/questionary/prompts/rawselect.py
create mode 100644 utils/questionary/questionary/prompts/select.py
create mode 100644 utils/questionary/questionary/prompts/text.py
create mode 100644 utils/questionary/questionary/py.typed
create mode 100644 utils/questionary/questionary/question.py
create mode 100644 utils/questionary/questionary/styles.py
create mode 100644 utils/questionary/questionary/utils.py
create mode 100644 utils/questionary/questionary/version.py
diff --git a/requirements.txt b/requirements.txt
index 9922074b..fa58ac5d 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -30,3 +30,4 @@ pluggy
httpcore
pydantic
jsonschema
+prompt-toolkit<=3.0.36,>=2.0
diff --git a/utils/questionary/NOTICE b/utils/questionary/NOTICE
new file mode 100644
index 00000000..051758ad
--- /dev/null
+++ b/utils/questionary/NOTICE
@@ -0,0 +1,51 @@
+Tom Bocklisch
+Copyright 2019 Tom Bocklisch
+
+----
+
+This product includes software from PyInquirer (https://github.com/CITGuru/PyInquirer),
+under the MIT License.
+
+Copyright 2018 Oyetoke Toby and contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+----
+
+This product includes software from whaaaaat (https://github.com/finklabs/whaaaaat),
+under the MIT License.
+
+Copyright 2016 Fink Labs GmbH and inquirerpy contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
diff --git a/utils/questionary/README.md b/utils/questionary/README.md
new file mode 100644
index 00000000..90d04d3c
--- /dev/null
+++ b/utils/questionary/README.md
@@ -0,0 +1,114 @@
+# Questionary
+
+[![Version](https://img.shields.io/pypi/v/questionary.svg)](https://pypi.org/project/questionary/)
+[![License](https://img.shields.io/pypi/l/questionary.svg)](#)
+[![Continuous Integration](https://github.com/tmbo/questionary/workflows/Continuous%20Integration/badge.svg)](#)
+[![Coverage](https://coveralls.io/repos/github/tmbo/questionary/badge.svg?branch=master)](https://coveralls.io/github/tmbo/questionary?branch=master)
+[![Supported Python Versions](https://img.shields.io/pypi/pyversions/questionary.svg)](https://pypi.python.org/pypi/questionary)
+[![Documentation](https://readthedocs.org/projects/questionary/badge/?version=latest)](https://questionary.readthedocs.io/en/latest/?badge=latest)
+
+✨ Questionary is a Python library for effortlessly building pretty command line interfaces ✨
+
+* [Features](#features)
+* [Installation](#installation)
+* [Usage](#usage)
+* [Documentation](#documentation)
+* [Support](#support)
+
+
+![Example](https://raw.githubusercontent.com/tmbo/questionary/master/docs/images/example.gif)
+
+```python3
+import questionary
+
+questionary.text("What's your first name").ask()
+questionary.password("What's your secret?").ask()
+questionary.confirm("Are you amazed?").ask()
+
+questionary.select(
+ "What do you want to do?",
+ choices=["Order a pizza", "Make a reservation", "Ask for opening hours"],
+).ask()
+
+questionary.rawselect(
+ "What do you want to do?",
+ choices=["Order a pizza", "Make a reservation", "Ask for opening hours"],
+).ask()
+
+questionary.checkbox(
+ "Select toppings", choices=["foo", "bar", "bazz"]
+).ask()
+
+questionary.path("Path to the projects version file").ask()
+```
+
+Used and supported by
+
+[](https://github.com/RasaHQ/rasa)
+
+## Features
+
+Questionary supports the following input prompts:
+
+ * [Text](https://questionary.readthedocs.io/en/stable/pages/types.html#text)
+ * [Password](https://questionary.readthedocs.io/en/stable/pages/types.html#password)
+ * [File Path](https://questionary.readthedocs.io/en/stable/pages/types.html#file-path)
+ * [Confirmation](https://questionary.readthedocs.io/en/stable/pages/types.html#confirmation)
+ * [Select](https://questionary.readthedocs.io/en/stable/pages/types.html#select)
+ * [Raw select](https://questionary.readthedocs.io/en/stable/pages/types.html#raw-select)
+ * [Checkbox](https://questionary.readthedocs.io/en/stable/pages/types.html#checkbox)
+ * [Autocomplete](https://questionary.readthedocs.io/en/stable/pages/types.html#autocomplete)
+
+There is also a helper to [print formatted text](https://questionary.readthedocs.io/en/stable/pages/types.html#printing-formatted-text)
+for when you want to spice up your printed messages a bit.
+
+## Installation
+
+Use the package manager [pip](https://pip.pypa.io/en/stable/) to install Questionary:
+
+```bash
+$ pip install questionary
+✨🎂✨
+```
+
+## Usage
+
+```python
+import questionary
+
+questionary.select(
+ "What do you want to do?",
+ choices=[
+ 'Order a pizza',
+ 'Make a reservation',
+ 'Ask for opening hours'
+ ]).ask() # returns value of selection
+```
+
+That's all it takes to create a prompt! Have a [look at the documentation](https://questionary.readthedocs.io/)
+for some more examples.
+
+## Documentation
+
+Documentation for Questionary is available [here](https://questionary.readthedocs.io/).
+
+## Support
+
+Please [open an issue](https://github.com/tmbo/questionary/issues/new)
+with enough information for us to reproduce your problem.
+A [minimal, reproducible example](https://stackoverflow.com/help/minimal-reproducible-example)
+would be very helpful.
+
+## Contributing
+
+Contributions are very much welcomed and appreciated. Head over to the documentation on [how to contribute](https://questionary.readthedocs.io/en/stable/pages/contributors.html#steps-for-submitting-code).
+
+## Authors and Acknowledgment
+
+Questionary is written and maintained by Tom Bocklisch and Kian Cross.
+
+It is based on the great work by [Oyetoke Toby](https://github.com/CITGuru/PyInquirer)
+and [Mark Fink](https://github.com/finklabs/whaaaaat).
+
+## License
+Licensed under the [MIT License](https://github.com/tmbo/questionary/blob/master/LICENSE). Copyright 2021 Tom Bocklisch.
diff --git a/utils/questionary/questionary/__init__.py b/utils/questionary/questionary/__init__.py
new file mode 100644
index 00000000..1d14551f
--- /dev/null
+++ b/utils/questionary/questionary/__init__.py
@@ -0,0 +1,54 @@
+# noinspection PyUnresolvedReferences
+from prompt_toolkit.styles import Style
+from prompt_toolkit.validation import ValidationError
+from prompt_toolkit.validation import Validator
+
+from .version import __version__
+from .form import Form
+from .form import FormField
+from .form import form
+from .prompt import prompt
+from .prompt import unsafe_prompt
+
+# import the shortcuts to create single question prompts
+from .prompts.autocomplete import autocomplete
+from .prompts.checkbox import checkbox
+from .prompts.common import Choice
+from .prompts.common import Separator
+from .prompts.common import print_formatted_text as print
+from .prompts.confirm import confirm
+from .prompts.password import password
+from .prompts.path import path
+from .prompts.press_any_key_to_continue import press_any_key_to_continue
+from .prompts.rawselect import rawselect
+from .prompts.select import select
+from .prompts.text import text
+from .question import Question
+
+__all__ = [
+ "__version__",
+ # question types
+ "autocomplete",
+ "checkbox",
+ "confirm",
+ "password",
+ "path",
+ "press_any_key_to_continue",
+ "rawselect",
+ "select",
+ "text",
+ # utility methods
+ "print",
+ "form",
+ "prompt",
+ "unsafe_prompt",
+ # commonly used classes
+ "Form",
+ "FormField",
+ "Question",
+ "Choice",
+ "Style",
+ "Separator",
+ "Validator",
+ "ValidationError",
+]
diff --git a/utils/questionary/questionary/constants.py b/utils/questionary/questionary/constants.py
new file mode 100644
index 00000000..768c481f
--- /dev/null
+++ b/utils/questionary/questionary/constants.py
@@ -0,0 +1,49 @@
+from . import Style
+
+# Value to display as an answer when "affirming" a confirmation question
+YES = "Yes"
+
+# Value to display as an answer when "denying" a confirmation question
+NO = "No"
+
+# Instruction text for a confirmation question (yes is default)
+YES_OR_NO = "(Y/n)"
+
+# Instruction text for a confirmation question (no is default)
+NO_OR_YES = "(y/N)"
+
+# Instruction for multiline input
+INSTRUCTION_MULTILINE = "(Finish with 'Alt+Enter' or 'Esc then Enter')\n>"
+
+# Selection token used to indicate the selection cursor in a list
+DEFAULT_SELECTED_POINTER = "»"
+
+# Item prefix to identify selected items in a checkbox list
+INDICATOR_SELECTED = "●"
+
+# Item prefix to identify unselected items in a checkbox list
+INDICATOR_UNSELECTED = "○"
+
+# Prefix displayed in front of questions
+DEFAULT_QUESTION_PREFIX = "?"
+
+# Message shown when a user aborts a question prompt using CTRL-C
+DEFAULT_KBI_MESSAGE = "Cancelled by user"
+
+# Default text shown when the input is invalid
+INVALID_INPUT = "Invalid input"
+
+# Default message style
+DEFAULT_STYLE = Style(
+ [
+ ("qmark", "fg:#5f819d"), # token in front of the question
+ ("question", "bold"), # question text
+ ("answer", "fg:#FF9D00 bold"), # submitted answer text behind the question
+ ("pointer", ""), # pointer used in select and checkbox prompts
+ ("selected", ""), # style for a selected item of a checkbox
+ ("separator", ""), # separator in lists
+ ("instruction", ""), # user instructions for select, rawselect, checkbox
+ ("text", ""), # any other text
+ ("instruction", ""), # user instructions for select, rawselect, checkbox
+ ]
+)
diff --git a/utils/questionary/questionary/form.py b/utils/questionary/questionary/form.py
new file mode 100644
index 00000000..b5d865f2
--- /dev/null
+++ b/utils/questionary/questionary/form.py
@@ -0,0 +1,122 @@
+
+from typing import Any
+from typing import Dict
+from typing import NamedTuple
+from typing import Sequence
+
+from .constants import DEFAULT_KBI_MESSAGE
+from .question import Question
+
+
+class FormField(NamedTuple):
+ """
+ Represents a question within a form
+
+ Args:
+ key: The name of the form field.
+ question: The question to ask in the form field.
+ """
+
+ key: str
+ question: Question
+
+
+def form(**kwargs: Question) -> "Form":
+ """Create a form with multiple questions.
+
+ The parameter name of a question will be the key for the answer in
+ the returned dict.
+
+ Args:
+ kwargs: Questions to ask in the form.
+ """
+ return Form(*(FormField(k, q) for k, q in kwargs.items()))
+
+
+class Form:
+ """Multi question prompts. Questions are asked one after another.
+
+ All the answers are returned as a dict with one entry per question.
+
+ This class should not be invoked directly, instead use :func:`form`.
+ """
+
+ form_fields: Sequence[FormField]
+
+ def __init__(self, *form_fields: FormField) -> None:
+ self.form_fields = form_fields
+
+ def unsafe_ask(self, patch_stdout: bool = False) -> Dict[str, Any]:
+ """Ask the questions synchronously and return user response.
+
+ Does not catch keyboard interrupts.
+
+ Args:
+ patch_stdout: Ensure that the prompt renders correctly if other threads
+ are printing to stdout.
+
+ Returns:
+ The answers from the form.
+ """
+ return {f.key: f.question.unsafe_ask(patch_stdout) for f in self.form_fields}
+
+ async def unsafe_ask_async(self, patch_stdout: bool = False) -> Dict[str, Any]:
+ """Ask the questions using asyncio and return user response.
+
+ Does not catch keyboard interrupts.
+
+ Args:
+ patch_stdout: Ensure that the prompt renders correctly if other threads
+ are printing to stdout.
+
+ Returns:
+ The answers from the form.
+ """
+ return {
+ f.key: await f.question.unsafe_ask_async(patch_stdout)
+ for f in self.form_fields
+ }
+
+ def ask(
+ self, patch_stdout: bool = False, kbi_msg: str = DEFAULT_KBI_MESSAGE
+ ) -> Dict[str, Any]:
+ """Ask the questions synchronously and return user response.
+
+ Args:
+ 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.
+
+ Returns:
+ The answers from the form.
+ """
+ try:
+ return self.unsafe_ask(patch_stdout)
+ except KeyboardInterrupt:
+ print("")
+ print(kbi_msg)
+ print("")
+ return {}
+
+ async def ask_async(
+ self, patch_stdout: bool = False, kbi_msg: str = DEFAULT_KBI_MESSAGE
+ ) -> Dict[str, Any]:
+ """Ask the questions using asyncio and return user response.
+
+ Args:
+ 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.
+
+ Returns:
+ The answers from the form.
+ """
+ try:
+ return await self.unsafe_ask_async(patch_stdout)
+ except KeyboardInterrupt:
+ print("")
+ print(kbi_msg)
+ print("")
+ return {}
diff --git a/utils/questionary/questionary/prompt.py b/utils/questionary/questionary/prompt.py
new file mode 100644
index 00000000..b4d18017
--- /dev/null
+++ b/utils/questionary/questionary/prompt.py
@@ -0,0 +1,236 @@
+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 . import utils
+from .constants import DEFAULT_KBI_MESSAGE
+from .prompts import AVAILABLE_PROMPTS
+from .prompts import prompt_by_name
+from .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
diff --git a/utils/questionary/questionary/prompts/__init__.py b/utils/questionary/questionary/prompts/__init__.py
new file mode 100644
index 00000000..ee7b8f7f
--- /dev/null
+++ b/utils/questionary/questionary/prompts/__init__.py
@@ -0,0 +1,29 @@
+from . import autocomplete
+from . import checkbox
+from . import confirm
+from . import password
+from . import path
+from . import press_any_key_to_continue
+from . import rawselect
+from . import select
+from . import text
+
+AVAILABLE_PROMPTS = {
+ "autocomplete": autocomplete.autocomplete,
+ "confirm": confirm.confirm,
+ "text": text.text,
+ "select": select.select,
+ "rawselect": rawselect.rawselect,
+ "password": password.password,
+ "checkbox": checkbox.checkbox,
+ "path": path.path,
+ "press_any_key_to_continue": press_any_key_to_continue.press_any_key_to_continue,
+ # backwards compatible names
+ "list": select.select,
+ "rawlist": rawselect.rawselect,
+ "input": text.text,
+}
+
+
+def prompt_by_name(name):
+ return AVAILABLE_PROMPTS.get(name)
diff --git a/utils/questionary/questionary/prompts/autocomplete.py b/utils/questionary/questionary/prompts/autocomplete.py
new file mode 100644
index 00000000..79d78baf
--- /dev/null
+++ b/utils/questionary/questionary/prompts/autocomplete.py
@@ -0,0 +1,214 @@
+from typing import Any
+from typing import Callable
+from typing import Dict
+from typing import Iterable
+from typing import List
+from typing import Optional
+from typing import Tuple
+from typing import Union
+
+from prompt_toolkit.completion import CompleteEvent
+from prompt_toolkit.completion import Completer
+from prompt_toolkit.completion import Completion
+from prompt_toolkit.document import Document
+from prompt_toolkit.formatted_text import HTML
+from prompt_toolkit.lexers import SimpleLexer
+from prompt_toolkit.shortcuts.prompt import CompleteStyle
+from prompt_toolkit.shortcuts.prompt import PromptSession
+from prompt_toolkit.styles import Style
+
+from ..constants import DEFAULT_QUESTION_PREFIX
+from ..prompts.common import build_validator
+from ..question import Question
+from ..styles import merge_styles_default
+
+
+class WordCompleter(Completer):
+ choices_source: Union[List[str], Callable[[], List[str]]]
+ ignore_case: bool
+ meta_information: Dict[str, Any]
+ match_middle: bool
+
+ def __init__(
+ self,
+ choices: Union[List[str], Callable[[], List[str]]],
+ ignore_case: bool = True,
+ meta_information: Optional[Dict[str, Any]] = None,
+ match_middle: bool = True,
+ ) -> None:
+ self.choices_source = choices
+ self.ignore_case = ignore_case
+ self.meta_information = meta_information or {}
+ self.match_middle = match_middle
+
+ def _choices(self) -> Iterable[str]:
+ return (
+ self.choices_source()
+ if callable(self.choices_source)
+ else self.choices_source
+ )
+
+ def _choice_matches(self, word_before_cursor: str, choice: str) -> int:
+ """Match index if found, -1 if not."""
+
+ if self.ignore_case:
+ choice = choice.lower()
+
+ if self.match_middle:
+ return choice.find(word_before_cursor)
+ elif choice.startswith(word_before_cursor):
+ return 0
+ else:
+ return -1
+
+ @staticmethod
+ def _display_for_choice(choice: str, index: int, word_before_cursor: str) -> HTML:
+ return HTML("{}{}{}").format(
+ choice[:index],
+ choice[index : index + len(word_before_cursor)], # noqa: E203
+ choice[index + len(word_before_cursor) : len(choice)], # noqa: E203
+ )
+
+ def get_completions(
+ self, document: Document, complete_event: CompleteEvent
+ ) -> Iterable[Completion]:
+ choices = self._choices()
+
+ # Get word/text before cursor.
+ word_before_cursor = document.text_before_cursor
+
+ if self.ignore_case:
+ word_before_cursor = word_before_cursor.lower()
+
+ for choice in choices:
+ index = self._choice_matches(word_before_cursor, choice)
+ if index == -1:
+ # didn't find a match
+ continue
+
+ display_meta = self.meta_information.get(choice, "")
+ display = self._display_for_choice(choice, index, word_before_cursor)
+
+ yield Completion(
+ choice,
+ start_position=-len(choice),
+ display=display.formatted_text,
+ display_meta=display_meta,
+ style="class:answer",
+ selected_style="class:selected",
+ )
+
+
+def autocomplete(
+ message: str,
+ choices: List[str],
+ default: str = "",
+ qmark: str = DEFAULT_QUESTION_PREFIX,
+ completer: Optional[Completer] = None,
+ meta_information: Optional[Dict[str, Any]] = None,
+ ignore_case: bool = True,
+ match_middle: bool = True,
+ complete_style: CompleteStyle = CompleteStyle.COLUMN,
+ validate: Any = None,
+ style: Optional[Style] = None,
+ **kwargs: Any,
+) -> Question:
+ """Prompt the user to enter a message with autocomplete help.
+
+ Example:
+ >>> import questionary
+ >>> questionary.autocomplete(
+ ... 'Choose ant specie',
+ ... choices=[
+ ... 'Camponotus pennsylvanicus',
+ ... 'Linepithema humile',
+ ... 'Eciton burchellii',
+ ... "Atta colombica",
+ ... 'Polyergus lucidus',
+ ... 'Polyergus rufescens',
+ ... ]).ask()
+ ? Choose ant specie Atta colombica
+ 'Atta colombica'
+
+ .. image:: ../images/autocomplete.gif
+
+ This is just a really basic example, the prompt can be customised using the
+ parameters.
+
+
+ Args:
+ message: Question text
+
+ choices: Items shown in the selection, this contains items as strings
+
+ default: Default return value (single value).
+
+ qmark: Question prefix displayed in front of the question.
+ By default this is a ``?``
+
+ completer: A prompt_toolkit :class:`prompt_toolkit.completion.Completion`
+ implementation. If not set, a questionary completer implementation
+ will be used.
+
+ meta_information: A dictionary with information/anything about choices.
+
+ ignore_case: If true autocomplete would ignore case.
+
+ match_middle: If true autocomplete would search in every string position
+ not only in string begin.
+
+ complete_style: How autocomplete menu would be shown, it could be ``COLUMN``
+ ``MULTI_COLUMN`` or ``READLINE_LIKE`` from
+ :class:`prompt_toolkit.shortcuts.CompleteStyle`.
+
+ validate: Require the entered value to pass a validation. The
+ value can not be submitted until the validator accepts
+ it (e.g. to check minimum password length).
+
+ This can either be a function accepting the input and
+ returning a boolean, or an class reference to a
+ subclass of the prompt toolkit Validator class.
+
+ style: A custom color and style for the question parts. You can
+ configure colors as well as font types for different elements.
+
+ Returns:
+ :class:`Question`: Question instance, ready to be prompted (using ``.ask()``).
+ """
+ merged_style = merge_styles_default([style])
+
+ def get_prompt_tokens() -> List[Tuple[str, str]]:
+ return [("class:qmark", qmark), ("class:question", " {} ".format(message))]
+
+ def get_meta_style(meta: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
+ if meta:
+ for key in meta:
+ meta[key] = HTML("{}").format(meta[key])
+
+ return meta
+
+ validator = build_validator(validate)
+
+ if completer is None:
+ if not choices:
+ raise ValueError("No choices is given, you should use Text question.")
+ # use the default completer
+ completer = WordCompleter(
+ choices,
+ ignore_case=ignore_case,
+ meta_information=get_meta_style(meta_information),
+ match_middle=match_middle,
+ )
+
+ p: PromptSession = PromptSession(
+ get_prompt_tokens,
+ lexer=SimpleLexer("class:answer"),
+ style=merged_style,
+ completer=completer,
+ validator=validator,
+ complete_style=complete_style,
+ **kwargs,
+ )
+ p.default_buffer.reset(Document(default))
+
+ return Question(p.app)
diff --git a/utils/questionary/questionary/prompts/checkbox.py b/utils/questionary/questionary/prompts/checkbox.py
new file mode 100644
index 00000000..35db6ff0
--- /dev/null
+++ b/utils/questionary/questionary/prompts/checkbox.py
@@ -0,0 +1,300 @@
+from typing import Any
+from typing import Callable
+from typing import Dict
+from typing import List
+from typing import Optional
+from typing import Sequence
+from typing import Tuple
+from typing import Union
+
+from prompt_toolkit.application import Application
+from prompt_toolkit.formatted_text import FormattedText
+from prompt_toolkit.key_binding import KeyBindings
+from prompt_toolkit.keys import Keys
+from prompt_toolkit.styles import Style
+
+from .. import utils
+from ..constants import DEFAULT_QUESTION_PREFIX
+from ..constants import DEFAULT_SELECTED_POINTER
+from ..constants import INVALID_INPUT
+from ..prompts import common
+from ..prompts.common import Choice
+from ..prompts.common import InquirerControl
+from ..prompts.common import Separator
+from ..question import Question
+from ..styles import merge_styles_default
+
+
+def checkbox(
+ message: str,
+ choices: Sequence[Union[str, Choice, Dict[str, Any]]],
+ default: Optional[str] = None,
+ validate: Callable[[List[str]], Union[bool, str]] = lambda a: True,
+ qmark: str = DEFAULT_QUESTION_PREFIX,
+ pointer: Optional[str] = DEFAULT_SELECTED_POINTER,
+ style: Optional[Style] = None,
+ initial_choice: Optional[Union[str, Choice, Dict[str, Any]]] = None,
+ use_arrow_keys: bool = True,
+ use_jk_keys: bool = True,
+ use_emacs_keys: bool = True,
+ instruction: Optional[str] = None,
+ show_description: bool = True,
+ **kwargs: Any,
+) -> Question:
+ """Ask the user to select from a list of items.
+
+ This is a multiselect, the user can choose one, none or many of the
+ items.
+
+ Example:
+ >>> import questionary
+ >>> questionary.checkbox(
+ ... 'Select toppings',
+ ... choices=[
+ ... "Cheese",
+ ... "Tomato",
+ ... "Pineapple",
+ ... ]).ask()
+ ? Select toppings done (2 selections)
+ ['Cheese', 'Pineapple']
+
+ .. image:: ../images/checkbox.gif
+
+ This is just a really basic example, the prompt can be customised using the
+ parameters.
+
+
+ Args:
+ message: Question text
+
+ choices: Items shown in the selection, this can contain :class:`Choice` or
+ or :class:`Separator` objects or simple items as strings. Passing
+ :class:`Choice` objects, allows you to configure the item more
+ (e.g. preselecting it or disabling it).
+
+ default: Default return value (single value). If you want to preselect
+ multiple items, use ``Choice("foo", checked=True)`` instead.
+
+ validate: Require the entered value to pass a validation. The
+ value can not be submitted until the validator accepts
+ it (e.g. to check minimum password length).
+
+ This should be a function accepting the input and
+ returning a boolean. Alternatively, the return value
+ may be a string (indicating failure), which contains
+ the error message to be displayed.
+
+ qmark: Question prefix displayed in front of the question.
+ By default this is a ``?``.
+
+ pointer: Pointer symbol in front of the currently highlighted element.
+ By default this is a ``»``.
+ Use ``None`` to disable it.
+
+ style: A custom color and style for the question parts. You can
+ configure colors as well as font types for different elements.
+
+ initial_choice: A value corresponding to a selectable item in the choices,
+ to initially set the pointer position to.
+
+ use_arrow_keys: Allow the user to select items from the list using
+ arrow keys.
+
+ use_jk_keys: Allow the user to select items from the list using
+ `j` (down) and `k` (up) keys.
+
+ use_emacs_keys: Allow the user to select items from the list using
+ `Ctrl+N` (down) and `Ctrl+P` (up) keys.
+ instruction: A message describing how to navigate the menu.
+
+ show_description: Display description of current selection if available.
+
+ Returns:
+ :class:`Question`: Question instance, ready to be prompted (using ``.ask()``).
+ """
+
+ if not (use_arrow_keys or use_jk_keys or use_emacs_keys):
+ raise ValueError(
+ "Some option to move the selection is required. Arrow keys or j/k or "
+ "Emacs keys."
+ )
+
+ merged_style = merge_styles_default(
+ [
+ # Disable the default inverted colours bottom-toolbar behaviour (for
+ # the error message). However it can be re-enabled with a custom
+ # style.
+ Style([("bottom-toolbar", "noreverse")]),
+ style,
+ ]
+ )
+
+ if not callable(validate):
+ raise ValueError("validate must be callable")
+
+ ic = InquirerControl(
+ choices,
+ default,
+ pointer=pointer,
+ initial_choice=initial_choice,
+ show_description=show_description,
+ )
+
+ def get_prompt_tokens() -> List[Tuple[str, str]]:
+ tokens = []
+
+ tokens.append(("class:qmark", qmark))
+ tokens.append(("class:question", " {} ".format(message)))
+
+ if ic.is_answered:
+ nbr_selected = len(ic.selected_options)
+ if nbr_selected == 0:
+ tokens.append(("class:answer", "done"))
+ elif nbr_selected == 1:
+ if isinstance(ic.get_selected_values()[0].title, list):
+ ts = ic.get_selected_values()[0].title
+ tokens.append(
+ (
+ "class:answer",
+ "".join([token[1] for token in ts]), # type:ignore
+ )
+ )
+ else:
+ tokens.append(
+ (
+ "class:answer",
+ "[{}]".format(ic.get_selected_values()[0].title),
+ )
+ )
+ else:
+ tokens.append(
+ ("class:answer", "done ({} selections)".format(nbr_selected))
+ )
+ else:
+ if instruction is not None:
+ tokens.append(("class:instruction", instruction))
+ else:
+ tokens.append(
+ (
+ "class:instruction",
+ "(Use arrow keys to move, "
+ " to select, "
+ " to toggle, "
+ " to invert)",
+ )
+ )
+ return tokens
+
+ def get_selected_values() -> List[Any]:
+ return [c.value for c in ic.get_selected_values()]
+
+ def perform_validation(selected_values: List[str]) -> bool:
+ verdict = validate(selected_values)
+ valid = verdict is True
+
+ if not valid:
+ if verdict is False:
+ error_text = INVALID_INPUT
+ else:
+ error_text = str(verdict)
+
+ error_message = FormattedText([("class:validation-toolbar", error_text)])
+
+ ic.error_message = (
+ error_message if not valid and ic.submission_attempted else None # type: ignore[assignment]
+ )
+
+ return valid
+
+ layout = common.create_inquirer_layout(ic, get_prompt_tokens, **kwargs)
+
+ bindings = KeyBindings()
+
+ @bindings.add(Keys.ControlQ, eager=True)
+ @bindings.add(Keys.ControlC, eager=True)
+ def _(event):
+ event.app.exit(exception=KeyboardInterrupt, style="class:aborting")
+
+ @bindings.add(" ", eager=True)
+ def toggle(_event):
+ pointed_choice = ic.get_pointed_at().value
+ if pointed_choice in ic.selected_options:
+ ic.selected_options.remove(pointed_choice)
+ else:
+ ic.selected_options.append(pointed_choice)
+
+ perform_validation(get_selected_values())
+
+ @bindings.add("i", eager=True)
+ def invert(_event):
+ inverted_selection = [
+ c.value
+ for c in ic.choices
+ if not isinstance(c, Separator)
+ and c.value not in ic.selected_options
+ and not c.disabled
+ ]
+ ic.selected_options = inverted_selection
+
+ perform_validation(get_selected_values())
+
+ @bindings.add("a", eager=True)
+ def all(_event):
+ all_selected = True # all choices have been selected
+ for c in ic.choices:
+ if (
+ not isinstance(c, Separator)
+ and c.value not in ic.selected_options
+ and not c.disabled
+ ):
+ # add missing ones
+ ic.selected_options.append(c.value)
+ all_selected = False
+ if all_selected:
+ ic.selected_options = []
+
+ perform_validation(get_selected_values())
+
+ def move_cursor_down(event):
+ ic.select_next()
+ while not ic.is_selection_valid():
+ ic.select_next()
+
+ def move_cursor_up(event):
+ ic.select_previous()
+ while not ic.is_selection_valid():
+ ic.select_previous()
+
+ if use_arrow_keys:
+ bindings.add(Keys.Down, eager=True)(move_cursor_down)
+ bindings.add(Keys.Up, eager=True)(move_cursor_up)
+
+ if use_jk_keys:
+ bindings.add("j", eager=True)(move_cursor_down)
+ bindings.add("k", eager=True)(move_cursor_up)
+
+ if use_emacs_keys:
+ bindings.add(Keys.ControlN, eager=True)(move_cursor_down)
+ bindings.add(Keys.ControlP, eager=True)(move_cursor_up)
+
+ @bindings.add(Keys.ControlM, eager=True)
+ def set_answer(event):
+ selected_values = get_selected_values()
+ ic.submission_attempted = True
+
+ if perform_validation(selected_values):
+ ic.is_answered = True
+ event.app.exit(result=selected_values)
+
+ @bindings.add(Keys.Any)
+ def other(_event):
+ """Disallow inserting other text."""
+
+ return Question(
+ Application(
+ layout=layout,
+ key_bindings=bindings,
+ style=merged_style,
+ **utils.used_kwargs(kwargs, Application.__init__),
+ )
+ )
diff --git a/utils/questionary/questionary/prompts/common.py b/utils/questionary/questionary/prompts/common.py
new file mode 100644
index 00000000..634136d0
--- /dev/null
+++ b/utils/questionary/questionary/prompts/common.py
@@ -0,0 +1,579 @@
+import inspect
+from typing import Any
+from typing import Callable
+from typing import Dict
+from typing import List
+from typing import Optional
+from typing import Sequence
+from typing import Tuple
+from typing import Union
+
+from prompt_toolkit import PromptSession
+from prompt_toolkit.filters import Always
+from prompt_toolkit.filters import Condition
+from prompt_toolkit.filters import IsDone
+from prompt_toolkit.layout import ConditionalContainer
+from prompt_toolkit.layout import FormattedTextControl
+from prompt_toolkit.layout import HSplit
+from prompt_toolkit.layout import Layout
+from prompt_toolkit.layout import Window
+from prompt_toolkit.styles import Style
+from prompt_toolkit.validation import ValidationError
+from prompt_toolkit.validation import Validator
+
+from ..constants import DEFAULT_SELECTED_POINTER
+from ..constants import DEFAULT_STYLE
+from ..constants import INDICATOR_SELECTED
+from ..constants import INDICATOR_UNSELECTED
+from ..constants import INVALID_INPUT
+
+# This is a cut-down version of `prompt_toolkit.formatted_text.AnyFormattedText`
+# which does not exist in v2 of prompt_toolkit
+FormattedText = Union[
+ str,
+ List[Tuple[str, str]],
+ List[Tuple[str, str, Callable[[Any], None]]],
+ None,
+]
+
+
+class Choice:
+ """One choice in a :meth:`select`, :meth:`rawselect` or :meth:`checkbox`.
+
+ Args:
+ title: Text shown in the selection list.
+
+ value: Value returned, when the choice is selected. If this argument
+ is `None` or unset, then the value of `title` is used.
+
+ disabled: If set, the choice can not be selected by the user. The
+ provided text is used to explain, why the selection is
+ disabled.
+
+ checked: Preselect this choice when displaying the options.
+
+ shortcut_key: Key shortcut used to select this item.
+
+ description: Optional description of the item that can be displayed.
+ """
+
+ title: FormattedText
+ """Display string for the choice"""
+
+ value: Optional[Any]
+ """Value of the choice"""
+
+ disabled: Optional[str]
+ """Whether the choice can be selected"""
+
+ checked: Optional[bool]
+ """Whether the choice is initially selected"""
+
+ shortcut_key: Optional[str]
+ """A shortcut key for the choice"""
+
+ description: Optional[str]
+ """Choice description"""
+
+ def __init__(
+ self,
+ title: FormattedText,
+ value: Optional[Any] = None,
+ disabled: Optional[str] = None,
+ checked: Optional[bool] = False,
+ shortcut_key: Optional[Union[str, bool]] = True,
+ description: Optional[str] = None,
+ ) -> None:
+ self.disabled = disabled
+ self.title = title
+ self.checked = checked if checked is not None else False
+ self.description = description
+
+ if value is not None:
+ self.value = value
+ elif isinstance(title, list):
+ self.value = "".join([token[1] for token in title])
+ else:
+ self.value = title
+
+ if shortcut_key is not None:
+ if isinstance(shortcut_key, bool):
+ self.auto_shortcut = shortcut_key
+ self.shortcut_key = None
+ else:
+ self.shortcut_key = str(shortcut_key)
+ self.auto_shortcut = False
+ else:
+ self.shortcut_key = None
+ self.auto_shortcut = True
+
+ @staticmethod
+ def build(c: Union[str, "Choice", Dict[str, Any]]) -> "Choice":
+ """Create a choice object from different representations.
+
+ Args:
+ c: Either a :obj:`str`, :class:`Choice` or :obj:`dict` with
+ ``name``, ``value``, ``disabled``, ``checked`` and
+ ``key`` properties.
+
+ Returns:
+ An instance of the :class:`Choice` object.
+ """
+
+ if isinstance(c, Choice):
+ return c
+ elif isinstance(c, str):
+ return Choice(c, c)
+ else:
+ return Choice(
+ c.get("name"),
+ c.get("value"),
+ c.get("disabled", None),
+ c.get("checked"),
+ c.get("key"),
+ c.get("description", None),
+ )
+
+ def get_shortcut_title(self):
+ if self.shortcut_key is None:
+ return "-) "
+ else:
+ return "{}) ".format(self.shortcut_key)
+
+
+class Separator(Choice):
+ """Used to space/separate choices group."""
+
+ default_separator: str = "-" * 15
+ """The default separator used if none is specified"""
+
+ line: str
+ """The string being used as a separator"""
+
+ def __init__(self, line: Optional[str] = None) -> None:
+ """Create a separator in a list.
+
+ Args:
+ line: Text to be displayed in the list, by default uses ``---``.
+ """
+
+ self.line = line or self.default_separator
+ super().__init__(self.line, None, "-")
+
+
+class InquirerControl(FormattedTextControl):
+ SHORTCUT_KEYS = [
+ "1",
+ "2",
+ "3",
+ "4",
+ "5",
+ "6",
+ "7",
+ "8",
+ "9",
+ "0",
+ "a",
+ "b",
+ "c",
+ "d",
+ "e",
+ "f",
+ "g",
+ "h",
+ "i",
+ "j",
+ "k",
+ "l",
+ "m",
+ "n",
+ "o",
+ "p",
+ "q",
+ "r",
+ "s",
+ "t",
+ "u",
+ "v",
+ "w",
+ "x",
+ "y",
+ "z",
+ ]
+
+ choices: List[Choice]
+ default: Optional[Union[str, Choice, Dict[str, Any]]]
+ selected_options: List[Any]
+ use_indicator: bool
+ use_shortcuts: bool
+ use_arrow_keys: bool
+ pointer: Optional[str]
+ pointed_at: int
+ is_answered: bool
+ show_description: bool
+
+ def __init__(
+ self,
+ choices: Sequence[Union[str, Choice, Dict[str, Any]]],
+ default: Optional[Union[str, Choice, Dict[str, Any]]] = None,
+ pointer: Optional[str] = DEFAULT_SELECTED_POINTER,
+ use_indicator: bool = True,
+ use_shortcuts: bool = False,
+ show_selected: bool = False,
+ show_description: bool = True,
+ use_arrow_keys: bool = True,
+ initial_choice: Optional[Union[str, Choice, Dict[str, Any]]] = None,
+ **kwargs: Any,
+ ):
+ self.use_indicator = use_indicator
+ self.use_shortcuts = use_shortcuts
+ self.show_selected = show_selected
+ self.show_description = show_description
+ self.use_arrow_keys = use_arrow_keys
+ self.default = default
+ self.pointer = pointer
+
+ if isinstance(default, Choice):
+ default = default.value
+
+ choices_values = [
+ choice.value for choice in choices if isinstance(choice, Choice)
+ ]
+
+ if (
+ default is not None
+ and default not in choices
+ and default not in choices_values
+ ):
+ raise ValueError(
+ f"Invalid `default` value passed. The value (`{default}`) "
+ f"does not exist in the set of choices. Please make sure the "
+ f"default value is one of the available choices."
+ )
+
+ if initial_choice is None:
+ pointed_at = None
+ elif initial_choice in choices:
+ pointed_at = choices.index(initial_choice)
+ elif initial_choice in choices_values:
+ for k, choice in enumerate(choices):
+ if isinstance(choice, Choice):
+ if choice.value == initial_choice:
+ pointed_at = k
+ break
+
+ else:
+ raise ValueError(
+ f"Invalid `initial_choice` value passed. The value "
+ f"(`{initial_choice}`) does not exist in "
+ f"the set of choices. Please make sure the initial value is "
+ f"one of the available choices."
+ )
+
+ self.is_answered = False
+ self.choices = []
+ self.submission_attempted = False
+ self.error_message = None
+ self.selected_options = []
+
+ self._init_choices(choices, pointed_at)
+ self._assign_shortcut_keys()
+
+ super().__init__(self._get_choice_tokens, **kwargs)
+
+ if not self.is_selection_valid():
+ raise ValueError(
+ f"Invalid 'initial_choice' value ('{initial_choice}'). "
+ f"It must be a selectable value."
+ )
+
+ def _is_selected(self, choice: Choice):
+ if isinstance(self.default, Choice):
+ compare_default = self.default == choice
+ else:
+ compare_default = self.default == choice.value
+ return choice.checked or compare_default and self.default is not None
+
+ def _assign_shortcut_keys(self):
+ available_shortcuts = self.SHORTCUT_KEYS[:]
+
+ # first, make sure we do not double assign a shortcut
+ for c in self.choices:
+ if c.shortcut_key is not None:
+ if c.shortcut_key in available_shortcuts:
+ available_shortcuts.remove(c.shortcut_key)
+ else:
+ raise ValueError(
+ "Invalid shortcut '{}'"
+ "for choice '{}'. Shortcuts "
+ "should be single characters or numbers. "
+ "Make sure that all your shortcuts are "
+ "unique.".format(c.shortcut_key, c.title)
+ )
+
+ shortcut_idx = 0
+ for c in self.choices:
+ if c.auto_shortcut and not c.disabled:
+ c.shortcut_key = available_shortcuts[shortcut_idx]
+ shortcut_idx += 1
+
+ if shortcut_idx == len(available_shortcuts):
+ break # fail gracefully if we run out of shortcuts
+
+ def _init_choices(
+ self,
+ choices: Sequence[Union[str, Choice, Dict[str, Any]]],
+ pointed_at: Optional[int],
+ ):
+ # helper to convert from question format to internal format
+ self.choices = []
+
+ if pointed_at is not None:
+ self.pointed_at = pointed_at
+
+ for i, c in enumerate(choices):
+ choice = Choice.build(c)
+
+ if self._is_selected(choice):
+ self.selected_options.append(choice.value)
+
+ if pointed_at is None and not choice.disabled:
+ # find the first (available) choice
+ self.pointed_at = pointed_at = i
+
+ self.choices.append(choice)
+
+ @property
+ def choice_count(self) -> int:
+ return len(self.choices)
+
+ def _get_choice_tokens(self):
+ tokens = []
+
+ def append(index: int, choice: Choice):
+ # use value to check if option has been selected
+ selected = choice.value in self.selected_options
+
+ if index == self.pointed_at:
+ if self.pointer is not None:
+ tokens.append(("class:pointer", " {} ".format(self.pointer)))
+ else:
+ tokens.append(("class:text", " " * 3))
+
+ tokens.append(("[SetCursorPosition]", ""))
+ else:
+ pointer_length = len(self.pointer) if self.pointer is not None else 1
+ tokens.append(("class:text", " " * (2 + pointer_length)))
+
+ if isinstance(choice, Separator):
+ tokens.append(("class:separator", "{}".format(choice.title)))
+ elif choice.disabled: # disabled
+ if isinstance(choice.title, list):
+ tokens.append(
+ ("class:selected" if selected else "class:disabled", "- ")
+ )
+ tokens.extend(choice.title)
+ else:
+ tokens.append(
+ (
+ "class:selected" if selected else "class:disabled",
+ "- {}".format(choice.title),
+ )
+ )
+
+ tokens.append(
+ (
+ "class:selected" if selected else "class:disabled",
+ "{}".format(
+ ""
+ if isinstance(choice.disabled, bool)
+ else " ({})".format(choice.disabled)
+ ),
+ )
+ )
+ else:
+ shortcut = choice.get_shortcut_title() if self.use_shortcuts else ""
+
+ if selected:
+ if self.use_indicator:
+ indicator = INDICATOR_SELECTED + " "
+ else:
+ indicator = ""
+
+ tokens.append(("class:selected", "{}".format(indicator)))
+ else:
+ if self.use_indicator:
+ indicator = INDICATOR_UNSELECTED + " "
+ else:
+ indicator = ""
+
+ tokens.append(("class:text", "{}".format(indicator)))
+
+ if isinstance(choice.title, list):
+ tokens.extend(choice.title)
+ elif selected:
+ tokens.append(
+ ("class:selected", "{}{}".format(shortcut, choice.title))
+ )
+ elif index == self.pointed_at:
+ tokens.append(
+ ("class:highlighted", "{}{}".format(shortcut, choice.title))
+ )
+ else:
+ tokens.append(("class:text", "{}{}".format(shortcut, choice.title)))
+
+ tokens.append(("", "\n"))
+
+ # prepare the select choices
+ for i, c in enumerate(self.choices):
+ append(i, c)
+
+ if self.show_selected:
+ current = self.get_pointed_at()
+
+ answer = current.get_shortcut_title() if self.use_shortcuts else ""
+
+ answer += (
+ current.title if isinstance(current.title, str) else current.title[0][1]
+ )
+
+ tokens.append(("class:text", " Answer: {}".format(answer)))
+
+ if self.show_description:
+ current = self.get_pointed_at()
+
+ description = current.description
+
+ if description is not None:
+ tokens.append(("class:text", " Description: {}".format(description)))
+ else:
+ tokens.pop() # Remove last newline.
+ return tokens
+
+ def is_selection_a_separator(self) -> bool:
+ selected = self.choices[self.pointed_at]
+ return isinstance(selected, Separator)
+
+ def is_selection_disabled(self) -> Optional[str]:
+ return self.choices[self.pointed_at].disabled
+
+ def is_selection_valid(self) -> bool:
+ return not self.is_selection_disabled() and not self.is_selection_a_separator()
+
+ def select_previous(self) -> None:
+ self.pointed_at = (self.pointed_at - 1) % self.choice_count
+
+ def select_next(self) -> None:
+ self.pointed_at = (self.pointed_at + 1) % self.choice_count
+
+ def get_pointed_at(self) -> Choice:
+ return self.choices[self.pointed_at]
+
+ def get_selected_values(self) -> List[Choice]:
+ # get values not labels
+ return [
+ c
+ for c in self.choices
+ if (not isinstance(c, Separator) and c.value in self.selected_options)
+ ]
+
+
+def build_validator(validate: Any) -> Optional[Validator]:
+ if validate:
+ if inspect.isclass(validate) and issubclass(validate, Validator):
+ return validate()
+ elif isinstance(validate, Validator):
+ return validate
+ elif callable(validate):
+
+ class _InputValidator(Validator):
+ def validate(self, document):
+ verdict = validate(document.text)
+ if verdict is not True:
+ if verdict is False:
+ verdict = INVALID_INPUT
+ raise ValidationError(
+ message=verdict, cursor_position=len(document.text)
+ )
+
+ return _InputValidator()
+ return None
+
+
+def _fix_unecessary_blank_lines(ps: PromptSession) -> None:
+ """This is a fix for additional empty lines added by prompt toolkit.
+
+ This assumes the layout of the default session doesn't change, if it
+ does, this needs an update."""
+
+ default_container = ps.layout.container
+
+ default_buffer_window = (
+ default_container.get_children()[0].content.get_children()[1].content # type: ignore[attr-defined]
+ )
+
+ assert isinstance(default_buffer_window, Window)
+ # this forces the main window to stay as small as possible, avoiding
+ # empty lines in selections
+ default_buffer_window.dont_extend_height = Always()
+ default_buffer_window.always_hide_cursor = Always()
+
+
+def create_inquirer_layout(
+ ic: InquirerControl,
+ get_prompt_tokens: Callable[[], List[Tuple[str, str]]],
+ **kwargs: Any,
+) -> Layout:
+ """Create a layout combining question and inquirer selection."""
+
+ ps: PromptSession = PromptSession(
+ get_prompt_tokens, reserve_space_for_menu=0, **kwargs
+ )
+ _fix_unecessary_blank_lines(ps)
+
+ validation_prompt: PromptSession = PromptSession(
+ bottom_toolbar=lambda: ic.error_message, **kwargs
+ )
+
+ return Layout(
+ HSplit(
+ [
+ ps.layout.container,
+ ConditionalContainer(Window(ic), filter=~IsDone()),
+ ConditionalContainer(
+ validation_prompt.layout.container,
+ filter=Condition(lambda: ic.error_message is not None),
+ ),
+ ]
+ )
+ )
+
+
+def print_formatted_text(text: str, style: Optional[str] = None, **kwargs: Any) -> None:
+ """Print formatted text.
+
+ Sometimes you want to spice up your printed messages a bit,
+ :meth:`questionary.print` is a helper to do just that.
+
+ Example:
+
+ >>> import questionary
+ >>> questionary.print("Hello World 🦄", style="bold italic fg:darkred")
+ Hello World 🦄
+
+ .. image:: ../images/print.gif
+
+ Args:
+ text: Text to be printed.
+ style: Style used for printing. The style argument uses the
+ prompt :ref:`toolkit style strings `.
+ """
+ from prompt_toolkit import print_formatted_text as pt_print
+ from prompt_toolkit.formatted_text import FormattedText as FText
+
+ if style is not None:
+ text_style = Style([("text", style)])
+ else:
+ text_style = DEFAULT_STYLE
+
+ pt_print(FText([("class:text", text)]), style=text_style, **kwargs)
diff --git a/utils/questionary/questionary/prompts/confirm.py b/utils/questionary/questionary/prompts/confirm.py
new file mode 100644
index 00000000..1fb2c52b
--- /dev/null
+++ b/utils/questionary/questionary/prompts/confirm.py
@@ -0,0 +1,133 @@
+from typing import Any
+from typing import Optional
+
+from prompt_toolkit import PromptSession
+from prompt_toolkit.formatted_text import to_formatted_text
+from prompt_toolkit.key_binding import KeyBindings
+from prompt_toolkit.keys import Keys
+from prompt_toolkit.styles import Style
+
+from ..constants import DEFAULT_QUESTION_PREFIX
+from ..constants import NO
+from ..constants import NO_OR_YES
+from ..constants import YES
+from ..constants import YES_OR_NO
+from ..question import Question
+from ..styles import merge_styles_default
+
+
+def confirm(
+ message: str,
+ default: bool = True,
+ qmark: str = DEFAULT_QUESTION_PREFIX,
+ style: Optional[Style] = None,
+ auto_enter: bool = True,
+ instruction: Optional[str] = None,
+ **kwargs: Any,
+) -> Question:
+ """A yes or no question. The user can either confirm or deny.
+
+ This question type can be used to prompt the user for a confirmation
+ of a yes-or-no question. If the user just hits enter, the default
+ value will be returned.
+
+ Example:
+ >>> import questionary
+ >>> questionary.confirm("Are you amazed?").ask()
+ ? Are you amazed? Yes
+ True
+
+ .. image:: ../images/confirm.gif
+
+ This is just a really basic example, the prompt can be customised using the
+ parameters.
+
+
+ Args:
+ message: Question text.
+
+ default: Default value will be returned if the user just hits
+ enter.
+
+ qmark: Question prefix displayed in front of the question.
+ By default this is a ``?``.
+
+ style: A custom color and style for the question parts. You can
+ configure colors as well as font types for different elements.
+
+ auto_enter: If set to `False`, the user needs to press the 'enter' key to
+ accept their answer. If set to `True`, a valid input will be
+ accepted without the need to press 'Enter'.
+
+ instruction: A message describing how to proceed through the
+ confirmation prompt.
+ Returns:
+ :class:`Question`: Question instance, ready to be prompted (using `.ask()`).
+ """
+ merged_style = merge_styles_default([style])
+
+ status = {"answer": None, "complete": False}
+
+ def get_prompt_tokens():
+ tokens = []
+
+ tokens.append(("class:qmark", qmark))
+ tokens.append(("class:question", " {} ".format(message)))
+
+ if instruction is not None:
+ tokens.append(("class:instruction", instruction))
+ elif not status["complete"]:
+ _instruction = YES_OR_NO if default else NO_OR_YES
+ tokens.append(("class:instruction", "{} ".format(_instruction)))
+
+ if status["answer"] is not None:
+ answer = YES if status["answer"] else NO
+ tokens.append(("class:answer", answer))
+
+ return to_formatted_text(tokens)
+
+ def exit_with_result(event):
+ status["complete"] = True
+ event.app.exit(result=status["answer"])
+
+ bindings = KeyBindings()
+
+ @bindings.add(Keys.ControlQ, eager=True)
+ @bindings.add(Keys.ControlC, eager=True)
+ def _(event):
+ event.app.exit(exception=KeyboardInterrupt, style="class:aborting")
+
+ @bindings.add("n")
+ @bindings.add("N")
+ def key_n(event):
+ status["answer"] = False
+ if auto_enter:
+ exit_with_result(event)
+
+ @bindings.add("y")
+ @bindings.add("Y")
+ def key_y(event):
+ status["answer"] = True
+ if auto_enter:
+ exit_with_result(event)
+
+ @bindings.add(Keys.ControlH)
+ def key_backspace(event):
+ status["answer"] = None
+
+ @bindings.add(Keys.ControlM, eager=True)
+ def set_answer(event):
+ if status["answer"] is None:
+ status["answer"] = default
+
+ exit_with_result(event)
+
+ @bindings.add(Keys.Any)
+ def other(event):
+ """Disallow inserting other text."""
+
+ return Question(
+ PromptSession(
+ get_prompt_tokens, key_bindings=bindings, style=merged_style, **kwargs
+ ).app
+ )
diff --git a/utils/questionary/questionary/prompts/password.py b/utils/questionary/questionary/prompts/password.py
new file mode 100644
index 00000000..35ae893a
--- /dev/null
+++ b/utils/questionary/questionary/prompts/password.py
@@ -0,0 +1,61 @@
+from typing import Any
+from typing import Optional
+
+from .. import Style
+from ..constants import DEFAULT_QUESTION_PREFIX
+from ..prompts import text
+from ..question import Question
+
+
+def password(
+ message: str,
+ default: str = "",
+ validate: Any = None,
+ qmark: str = DEFAULT_QUESTION_PREFIX,
+ style: Optional[Style] = None,
+ **kwargs: Any,
+) -> Question:
+ """A text input where a user can enter a secret which won't be displayed on the CLI.
+
+ This question type can be used to prompt the user for information
+ that should not be shown in the command line. The typed text will be
+ replaced with ``*``.
+
+ Example:
+ >>> import questionary
+ >>> questionary.password("What's your secret?").ask()
+ ? What's your secret? ********
+ 'secret42'
+
+ .. image:: ../images/password.gif
+
+ This is just a really basic example, the prompt can be customised using the
+ parameters.
+
+ Args:
+ message: Question text.
+
+ default: Default value will be returned if the user just hits
+ enter.
+
+ validate: Require the entered value to pass a validation. The
+ value can not be submitted until the validator accepts
+ it (e.g. to check minimum password length).
+
+ This can either be a function accepting the input and
+ returning a boolean, or an class reference to a
+ subclass of the prompt toolkit Validator class.
+
+ qmark: Question prefix displayed in front of the question.
+ By default this is a ``?``.
+
+ style: A custom color and style for the question parts. You can
+ configure colors as well as font types for different elements.
+
+ Returns:
+ :class:`Question`: Question instance, ready to be prompted (using ``.ask()``).
+ """
+
+ return text.text(
+ message, default, validate, qmark, style, is_password=True, **kwargs
+ )
diff --git a/utils/questionary/questionary/prompts/path.py b/utils/questionary/questionary/prompts/path.py
new file mode 100644
index 00000000..e43bd0fd
--- /dev/null
+++ b/utils/questionary/questionary/prompts/path.py
@@ -0,0 +1,243 @@
+import os
+from typing import Any
+from typing import Callable
+from typing import Iterable
+from typing import List
+from typing import Optional
+from typing import Tuple
+
+from prompt_toolkit.completion import CompleteEvent
+from prompt_toolkit.completion import Completion
+from prompt_toolkit.completion import PathCompleter
+from prompt_toolkit.completion.base import Completer
+from prompt_toolkit.document import Document
+from prompt_toolkit.key_binding import KeyBindings
+from prompt_toolkit.key_binding.key_processor import KeyPressEvent
+from prompt_toolkit.keys import Keys
+from prompt_toolkit.lexers import SimpleLexer
+from prompt_toolkit.shortcuts.prompt import CompleteStyle
+from prompt_toolkit.shortcuts.prompt import PromptSession
+from prompt_toolkit.styles import Style
+
+from ..constants import DEFAULT_QUESTION_PREFIX
+from ..prompts.common import build_validator
+from ..question import Question
+from ..styles import merge_styles_default
+
+
+class GreatUXPathCompleter(PathCompleter):
+ """Wraps :class:`prompt_toolkit.completion.PathCompleter`.
+
+ Makes sure completions for directories end with a path separator. Also make sure
+ the right path separator is used. Checks if `get_paths` returns list of existing
+ directories.
+ """
+
+ def __init__(
+ self,
+ only_directories: bool = False,
+ get_paths: Optional[Callable[[], List[str]]] = None,
+ file_filter: Optional[Callable[[str], bool]] = None,
+ min_input_len: int = 0,
+ expanduser: bool = False,
+ ) -> None:
+ """Adds validation of 'get_paths' to :class:`prompt_toolkit.completion.PathCompleter`.
+
+ Args:
+ only_directories (bool): If True, only directories will be
+ returned, but no files. Defaults to False.
+ get_paths (Callable[[], List[str]], optional): Callable which
+ returns a list of directories to look into when the user enters a
+ relative path. If None, set to (lambda: ["."]). Defaults to None.
+ file_filter (Callable[[str], bool], optional): Callable which
+ takes a filename and returns whether this file should show up in the
+ completion. ``None`` when no filtering has to be done. Defaults to None.
+ min_input_len (int): Don't do autocompletion when the input string
+ is shorter. Defaults to 0.
+ expanduser (bool): If True, tilde (~) is expanded. Defaults to
+ False.
+
+ Raises:
+ ValueError: If any of the by `get_paths` returned directories does not
+ exist.
+ """
+ # if get_paths is None, make it return the current working dir
+ get_paths = get_paths or (lambda: ["."])
+ # validation of get_paths
+ for current_path in get_paths():
+ if not os.path.isdir(current_path):
+ raise (
+ ValueError(
+ "\n Completer for file paths 'get_paths' must return only existing directories, but"
+ f" '{current_path}' does not exist."
+ )
+ )
+ # call PathCompleter __init__
+ super().__init__(
+ only_directories=only_directories,
+ get_paths=get_paths,
+ file_filter=file_filter,
+ min_input_len=min_input_len,
+ expanduser=expanduser,
+ )
+
+ def get_completions(
+ self, document: Document, complete_event: CompleteEvent
+ ) -> Iterable[Completion]:
+ """Get completions.
+
+ Wraps :class:`prompt_toolkit.completion.PathCompleter`. Makes sure completions
+ for directories end with a path separator. Also make sure the right path
+ separator is used.
+ """
+ completions = super(GreatUXPathCompleter, self).get_completions(
+ document, complete_event
+ )
+
+ for completion in completions:
+ # check if the display value ends with a path separator.
+ # first check if display is properly set
+ styled_display = completion.display[0]
+ # styled display is a formatted text (a tuple of the text and its style)
+ # second tuple entry is the text
+ if styled_display[1][-1] == "/":
+ # replace separator with the OS specific one
+ display_text = styled_display[1][:-1] + os.path.sep
+ # update the styled display with the modified text
+ completion.display[0] = (styled_display[0], display_text)
+ # append the separator to the text as well - unclear why the normal
+ # path completer omits it from the text. this improves UX for the
+ # user, as they don't need to type the separator after auto-completing
+ # a directory
+ completion.text += os.path.sep
+ yield completion
+
+
+def path(
+ message: str,
+ default: str = "",
+ qmark: str = DEFAULT_QUESTION_PREFIX,
+ validate: Any = None,
+ completer: Optional[Completer] = None,
+ style: Optional[Style] = None,
+ only_directories: bool = False,
+ get_paths: Optional[Callable[[], List[str]]] = None,
+ file_filter: Optional[Callable[[str], bool]] = None,
+ complete_style: CompleteStyle = CompleteStyle.MULTI_COLUMN,
+ **kwargs: Any,
+) -> Question:
+ """A text input for a file or directory path with autocompletion enabled.
+
+ Example:
+ >>> import questionary
+ >>> questionary.path(
+ >>> "What's the path to the projects version file?"
+ >>> ).ask()
+ ? What's the path to the projects version file? ./pyproject.toml
+ './pyproject.toml'
+
+ .. image:: ../images/path.gif
+
+ This is just a really basic example, the prompt can be customized using the
+ parameters.
+
+ Args:
+ message: Question text.
+
+ default: Default return value (single value).
+
+ qmark: Question prefix displayed in front of the question.
+ By default this is a ``?``.
+
+ complete_style: How autocomplete menu would be shown, it could be ``COLUMN``
+ ``MULTI_COLUMN`` or ``READLINE_LIKE`` from
+ :class:`prompt_toolkit.shortcuts.CompleteStyle`.
+
+ validate: Require the entered value to pass a validation. The
+ value can not be submitted until the validator accepts
+ it (e.g. to check minimum password length).
+
+ This can either be a function accepting the input and
+ returning a boolean, or an class reference to a
+ subclass of the prompt toolkit Validator class.
+
+ completer: A custom completer to use in the prompt. For more information,
+ see `this `_.
+
+ style: A custom color and style for the question parts. You can
+ configure colors as well as font types for different elements.
+
+ only_directories: Only show directories in auto completion. This option
+ does not do anything if a custom ``completer`` is
+ passed.
+
+ get_paths: Set a callable to generate paths to traverse for suggestions. This option
+ does not do anything if a custom ``completer`` is
+ passed.
+
+ file_filter: Optional callable to filter suggested paths. Only paths
+ where the passed callable evaluates to ``True`` will show up in
+ the suggested paths. This does not validate the typed path, e.g.
+ it is still possible for the user to enter a path manually, even
+ though this filter evaluates to ``False``. If in addition to
+ filtering suggestions you also want to validate the result, use
+ ``validate`` in combination with the ``file_filter``.
+
+ Returns:
+ :class:`Question`: Question instance, ready to be prompted (using ``.ask()``).
+ """ # noqa: W505, E501
+ merged_style = merge_styles_default([style])
+
+ def get_prompt_tokens() -> List[Tuple[str, str]]:
+ return [("class:qmark", qmark), ("class:question", " {} ".format(message))]
+
+ validator = build_validator(validate)
+
+ completer = completer or GreatUXPathCompleter(
+ get_paths=get_paths,
+ only_directories=only_directories,
+ file_filter=file_filter,
+ expanduser=True,
+ )
+
+ bindings = KeyBindings()
+
+ @bindings.add(Keys.ControlM, eager=True)
+ def set_answer(event: KeyPressEvent):
+ if event.current_buffer.complete_state is not None:
+ event.current_buffer.complete_state = None
+ elif event.app.current_buffer.validate(set_cursor=True):
+ # When the validation succeeded, accept the input.
+ result_path = event.app.current_buffer.document.text
+ if result_path.endswith(os.path.sep):
+ result_path = result_path[:-1]
+
+ event.app.exit(result=result_path)
+ event.app.current_buffer.append_to_history()
+
+ @bindings.add(os.path.sep, eager=True)
+ def next_segment(event: KeyPressEvent):
+ b = event.app.current_buffer
+
+ if b.complete_state:
+ b.complete_state = None
+
+ current_path = b.document.text
+ if not current_path.endswith(os.path.sep):
+ b.insert_text(os.path.sep)
+
+ b.start_completion(select_first=False)
+
+ p: PromptSession = PromptSession(
+ get_prompt_tokens,
+ lexer=SimpleLexer("class:answer"),
+ style=merged_style,
+ completer=completer,
+ validator=validator,
+ complete_style=complete_style,
+ key_bindings=bindings,
+ **kwargs,
+ )
+ p.default_buffer.reset(Document(default))
+
+ return Question(p.app)
diff --git a/utils/questionary/questionary/prompts/press_any_key_to_continue.py b/utils/questionary/questionary/prompts/press_any_key_to_continue.py
new file mode 100644
index 00000000..402eaeaf
--- /dev/null
+++ b/utils/questionary/questionary/prompts/press_any_key_to_continue.py
@@ -0,0 +1,61 @@
+from typing import Any
+from typing import Optional
+
+from prompt_toolkit import PromptSession
+from prompt_toolkit.formatted_text import to_formatted_text
+from prompt_toolkit.key_binding import KeyBindings
+from prompt_toolkit.keys import Keys
+from prompt_toolkit.styles import Style
+
+from ..question import Question
+from ..styles import merge_styles_default
+
+
+def press_any_key_to_continue(
+ message: Optional[str] = None,
+ style: Optional[Style] = None,
+ **kwargs: Any,
+):
+ """Wait until user presses any key to continue.
+
+ Example:
+ >>> import questionary
+ >>> questionary.press_any_key_to_continue().ask()
+ Press any key to continue...
+ ''
+
+ Args:
+ message: Question text. Defaults to ``"Press any key to continue..."``
+
+ style: A custom color and style for the question parts. You can
+ configure colors as well as font types for different elements.
+
+ Returns:
+ :class:`Question`: Question instance, ready to be prompted (using ``.ask()``).
+ """
+ merged_style = merge_styles_default([style])
+
+ if message is None:
+ message = "Press any key to continue..."
+
+ def get_prompt_tokens():
+ tokens = []
+
+ tokens.append(("class:question", f" {message} "))
+
+ return to_formatted_text(tokens)
+
+ def exit_with_result(event):
+ event.app.exit(result=None)
+
+ bindings = KeyBindings()
+
+ @bindings.add(Keys.Any)
+ def any_key(event):
+ exit_with_result(event)
+
+ return Question(
+ PromptSession(
+ get_prompt_tokens, key_bindings=bindings, style=merged_style, **kwargs
+ ).app
+ )
diff --git a/utils/questionary/questionary/prompts/rawselect.py b/utils/questionary/questionary/prompts/rawselect.py
new file mode 100644
index 00000000..3fe74573
--- /dev/null
+++ b/utils/questionary/questionary/prompts/rawselect.py
@@ -0,0 +1,79 @@
+from typing import Any
+from typing import Dict
+from typing import Optional
+from typing import Sequence
+from typing import Union
+
+from prompt_toolkit.styles import Style
+
+from ..constants import DEFAULT_QUESTION_PREFIX
+from ..constants import DEFAULT_SELECTED_POINTER
+from ..prompts import select
+from ..prompts.common import Choice
+from ..question import Question
+
+
+def rawselect(
+ message: str,
+ choices: Sequence[Union[str, Choice, Dict[str, Any]]],
+ default: Optional[str] = None,
+ qmark: str = DEFAULT_QUESTION_PREFIX,
+ pointer: Optional[str] = DEFAULT_SELECTED_POINTER,
+ style: Optional[Style] = None,
+ **kwargs: Any,
+) -> Question:
+ """Ask the user to select one item from a list of choices using shortcuts.
+
+ The user can only select one option.
+
+ Example:
+ >>> import questionary
+ >>> questionary.rawselect(
+ ... "What do you want to do?",
+ ... choices=[
+ ... "Order a pizza",
+ ... "Make a reservation",
+ ... "Ask for opening hours"
+ ... ]).ask()
+ ? What do you want to do? Order a pizza
+ 'Order a pizza'
+
+ .. image:: ../images/rawselect.gif
+
+ This is just a really basic example, the prompt can be customised using the
+ parameters.
+
+ Args:
+ message: Question text.
+
+ choices: Items shown in the selection, this can contain :class:`Choice` or
+ or :class:`Separator` objects or simple items as strings. Passing
+ :class:`Choice` objects, allows you to configure the item more
+ (e.g. preselecting it or disabling it).
+
+ default: Default return value (single value).
+
+ qmark: Question prefix displayed in front of the question.
+ By default this is a ``?``.
+
+ pointer: Pointer symbol in front of the currently highlighted element.
+ By default this is a ``»``.
+ Use ``None`` to disable it.
+
+ style: A custom color and style for the question parts. You can
+ configure colors as well as font types for different elements.
+
+ Returns:
+ :class:`Question`: Question instance, ready to be prompted (using ``.ask()``).
+ """
+ return select.select(
+ message,
+ choices,
+ default,
+ qmark,
+ pointer,
+ style,
+ use_shortcuts=True,
+ use_arrow_keys=False,
+ **kwargs,
+ )
diff --git a/utils/questionary/questionary/prompts/select.py b/utils/questionary/questionary/prompts/select.py
new file mode 100644
index 00000000..6f3ef8a6
--- /dev/null
+++ b/utils/questionary/questionary/prompts/select.py
@@ -0,0 +1,258 @@
+# -*- coding: utf-8 -*-
+
+from typing import Any
+from typing import Dict
+from typing import Optional
+from typing import Sequence
+from typing import Union
+
+from prompt_toolkit.application import Application
+from prompt_toolkit.key_binding import KeyBindings
+from prompt_toolkit.keys import Keys
+from prompt_toolkit.styles import Style
+
+from .. import utils
+from ..constants import DEFAULT_QUESTION_PREFIX
+from ..constants import DEFAULT_SELECTED_POINTER
+from ..prompts import common
+from ..prompts.common import Choice
+from ..prompts.common import InquirerControl
+from ..prompts.common import Separator
+from ..question import Question
+from ..styles import merge_styles_default
+
+
+def select(
+ message: str,
+ choices: Sequence[Union[str, Choice, Dict[str, Any]]],
+ default: Optional[Union[str, Choice, Dict[str, Any]]] = None,
+ qmark: str = DEFAULT_QUESTION_PREFIX,
+ pointer: Optional[str] = DEFAULT_SELECTED_POINTER,
+ style: Optional[Style] = None,
+ use_shortcuts: bool = False,
+ use_arrow_keys: bool = True,
+ use_indicator: bool = False,
+ use_jk_keys: bool = True,
+ use_emacs_keys: bool = True,
+ show_selected: bool = False,
+ show_description: bool = True,
+ instruction: Optional[str] = None,
+ **kwargs: Any,
+) -> Question:
+ """A list of items to select **one** option from.
+
+ The user can pick one option and confirm it (if you want to allow
+ the user to select multiple options, use :meth:`questionary.checkbox` instead).
+
+ Example:
+ >>> import questionary
+ >>> questionary.select(
+ ... "What do you want to do?",
+ ... choices=[
+ ... "Order a pizza",
+ ... "Make a reservation",
+ ... "Ask for opening hours"
+ ... ]).ask()
+ ? What do you want to do? Order a pizza
+ 'Order a pizza'
+
+ .. image:: ../images/select.gif
+
+ This is just a really basic example, the prompt can be customised using the
+ parameters.
+
+
+ Args:
+ message: Question text
+
+ choices: Items shown in the selection, this can contain :class:`Choice` or
+ or :class:`Separator` objects or simple items as strings. Passing
+ :class:`Choice` objects, allows you to configure the item more
+ (e.g. preselecting it or disabling it).
+
+ default: A value corresponding to a selectable item in the choices,
+ to initially set the pointer position to.
+
+ qmark: Question prefix displayed in front of the question.
+ By default this is a ``?``.
+
+ pointer: Pointer symbol in front of the currently highlighted element.
+ By default this is a ``»``.
+ Use ``None`` to disable it.
+
+ instruction: A hint on how to navigate the menu.
+ It's ``(Use shortcuts)`` if only ``use_shortcuts`` is set
+ to True, ``(Use arrow keys or shortcuts)`` if ``use_arrow_keys``
+ & ``use_shortcuts`` are set and ``(Use arrow keys)`` by default.
+
+ style: A custom color and style for the question parts. You can
+ configure colors as well as font types for different elements.
+
+ use_indicator: Flag to enable the small indicator in front of the
+ list highlighting the current location of the selection
+ cursor.
+
+ use_shortcuts: Allow the user to select items from the list using
+ shortcuts. The shortcuts will be displayed in front of
+ the list items. Arrow keys, j/k keys and shortcuts are
+ not mutually exclusive.
+
+ use_arrow_keys: Allow the user to select items from the list using
+ arrow keys. Arrow keys, j/k keys and shortcuts are not
+ mutually exclusive.
+
+ use_jk_keys: Allow the user to select items from the list using
+ `j` (down) and `k` (up) keys. Arrow keys, j/k keys and
+ shortcuts are not mutually exclusive.
+
+ use_emacs_keys: Allow the user to select items from the list using
+ `Ctrl+N` (down) and `Ctrl+P` (up) keys. Arrow keys, j/k keys,
+ emacs keys and shortcuts are not mutually exclusive.
+
+ show_selected: Display current selection choice at the bottom of list.
+
+ show_description: Display description of current selection if available.
+
+ Returns:
+ :class:`Question`: Question instance, ready to be prompted (using ``.ask()``).
+ """
+ if not (use_arrow_keys or use_shortcuts or use_jk_keys or use_emacs_keys):
+ raise ValueError(
+ (
+ "Some option to move the selection is required. "
+ "Arrow keys, j/k keys, emacs keys, or shortcuts."
+ )
+ )
+
+ if use_shortcuts and use_jk_keys:
+ if any(getattr(c, "shortcut_key", "") in ["j", "k"] for c in choices):
+ raise ValueError(
+ "A choice is trying to register j/k as a "
+ "shortcut key when they are in use as arrow keys "
+ "disable one or the other."
+ )
+
+ if choices is None or len(choices) == 0:
+ raise ValueError("A list of choices needs to be provided.")
+
+ if use_shortcuts and len(choices) > len(InquirerControl.SHORTCUT_KEYS):
+ raise ValueError(
+ "A list with shortcuts supports a maximum of {} "
+ "choices as this is the maximum number "
+ "of keyboard shortcuts that are available. You"
+ "provided {} choices!"
+ "".format(len(InquirerControl.SHORTCUT_KEYS), len(choices))
+ )
+
+ merged_style = merge_styles_default([style])
+
+ ic = InquirerControl(
+ choices,
+ default,
+ pointer=pointer,
+ use_indicator=use_indicator,
+ use_shortcuts=use_shortcuts,
+ show_selected=show_selected,
+ show_description=show_description,
+ use_arrow_keys=use_arrow_keys,
+ initial_choice=default,
+ )
+
+ def get_prompt_tokens():
+ # noinspection PyListCreation
+ tokens = [("class:qmark", qmark), ("class:question", " {} ".format(message))]
+
+ if ic.is_answered:
+ if isinstance(ic.get_pointed_at().title, list):
+ tokens.append(
+ (
+ "class:answer",
+ "".join([token[1] for token in ic.get_pointed_at().title]),
+ )
+ )
+ else:
+ tokens.append(("class:answer", ic.get_pointed_at().title))
+ else:
+ if instruction:
+ tokens.append(("class:instruction", instruction))
+ else:
+ if use_shortcuts and use_arrow_keys:
+ instruction_msg = "(Use shortcuts or arrow keys)"
+ elif use_shortcuts and not use_arrow_keys:
+ instruction_msg = "(Use shortcuts)"
+ else:
+ instruction_msg = "(Use arrow keys)"
+ tokens.append(("class:instruction", instruction_msg))
+
+ return tokens
+
+ layout = common.create_inquirer_layout(ic, get_prompt_tokens, **kwargs)
+
+ bindings = KeyBindings()
+
+ @bindings.add(Keys.ControlQ, eager=True)
+ @bindings.add(Keys.ControlC, eager=True)
+ def _(event):
+ event.app.exit(exception=KeyboardInterrupt, style="class:aborting")
+
+ if use_shortcuts:
+ # add key bindings for choices
+ for i, c in enumerate(ic.choices):
+ if c.shortcut_key is None and not c.disabled and not use_arrow_keys:
+ raise RuntimeError(
+ "{} does not have a shortcut and arrow keys "
+ "for movement are disabled. "
+ "This choice is not reachable.".format(c.title)
+ )
+ if isinstance(c, Separator) or c.shortcut_key is None:
+ continue
+
+ # noinspection PyShadowingNames
+ def _reg_binding(i, keys):
+ # trick out late evaluation with a "function factory":
+ # https://stackoverflow.com/a/3431699
+ @bindings.add(keys, eager=True)
+ def select_choice(event):
+ ic.pointed_at = i
+
+ _reg_binding(i, c.shortcut_key)
+
+ def move_cursor_down(event):
+ ic.select_next()
+ while not ic.is_selection_valid():
+ ic.select_next()
+
+ def move_cursor_up(event):
+ ic.select_previous()
+ while not ic.is_selection_valid():
+ ic.select_previous()
+
+ if use_arrow_keys:
+ bindings.add(Keys.Down, eager=True)(move_cursor_down)
+ bindings.add(Keys.Up, eager=True)(move_cursor_up)
+
+ if use_jk_keys:
+ bindings.add("j", eager=True)(move_cursor_down)
+ bindings.add("k", eager=True)(move_cursor_up)
+
+ if use_emacs_keys:
+ bindings.add(Keys.ControlN, eager=True)(move_cursor_down)
+ bindings.add(Keys.ControlP, eager=True)(move_cursor_up)
+
+ @bindings.add(Keys.ControlM, eager=True)
+ def set_answer(event):
+ ic.is_answered = True
+ event.app.exit(result=ic.get_pointed_at().value)
+
+ @bindings.add(Keys.Any)
+ def other(event):
+ """Disallow inserting other text."""
+
+ return Question(
+ Application(
+ layout=layout,
+ key_bindings=bindings,
+ style=merged_style,
+ **utils.used_kwargs(kwargs, Application.__init__),
+ )
+ )
diff --git a/utils/questionary/questionary/prompts/text.py b/utils/questionary/questionary/prompts/text.py
new file mode 100644
index 00000000..0812104c
--- /dev/null
+++ b/utils/questionary/questionary/prompts/text.py
@@ -0,0 +1,101 @@
+from typing import Any
+from typing import List
+from typing import Optional
+from typing import Tuple
+
+from prompt_toolkit.document import Document
+from prompt_toolkit.lexers import Lexer
+from prompt_toolkit.lexers import SimpleLexer
+from prompt_toolkit.shortcuts.prompt import PromptSession
+from prompt_toolkit.styles import Style
+
+from ..constants import DEFAULT_QUESTION_PREFIX
+from ..constants import INSTRUCTION_MULTILINE
+from ..prompts.common import build_validator
+from ..question import Question
+from ..styles import merge_styles_default
+
+
+def text(
+ message: str,
+ default: str = "",
+ validate: Any = None,
+ qmark: str = DEFAULT_QUESTION_PREFIX,
+ style: Optional[Style] = None,
+ multiline: bool = False,
+ instruction: Optional[str] = None,
+ lexer: Optional[Lexer] = None,
+ **kwargs: Any,
+) -> Question:
+ """Prompt the user to enter a free text message.
+
+ This question type can be used to prompt the user for some text input.
+
+ Example:
+ >>> import questionary
+ >>> questionary.text("What's your first name?").ask()
+ ? What's your first name? Tom
+ 'Tom'
+
+ .. image:: ../images/text.gif
+
+ This is just a really basic example, the prompt can be customised using the
+ parameters.
+
+ Args:
+ message: Question text.
+
+ default: Default value will be returned if the user just hits
+ enter.
+
+ validate: Require the entered value to pass a validation. The
+ value can not be submitted until the validator accepts
+ it (e.g. to check minimum password length).
+
+ This can either be a function accepting the input and
+ returning a boolean, or an class reference to a
+ subclass of the prompt toolkit Validator class.
+
+ qmark: Question prefix displayed in front of the question.
+ By default this is a ``?``.
+
+ style: A custom color and style for the question parts. You can
+ configure colors as well as font types for different elements.
+
+ multiline: If ``True``, multiline input will be enabled.
+
+ instruction: Write instructions for the user if needed. If ``None``
+ and ``multiline=True``, some instructions will appear.
+
+ lexer: Supply a valid lexer to style the answer. Leave empty to
+ use a simple one by default.
+
+ kwargs: Additional arguments, they will be passed to prompt toolkit.
+
+ Returns:
+ :class:`Question`: Question instance, ready to be prompted (using ``.ask()``).
+ """
+ merged_style = merge_styles_default([style])
+ lexer = lexer or SimpleLexer("class:answer")
+ validator = build_validator(validate)
+
+ if instruction is None and multiline:
+ instruction = INSTRUCTION_MULTILINE
+
+ def get_prompt_tokens() -> List[Tuple[str, str]]:
+ result = [("class:qmark", qmark), ("class:question", " {} ".format(message))]
+ if instruction:
+ result.append(("class:instruction", " {} ".format(instruction)))
+ return result
+
+ p: PromptSession = PromptSession(
+ get_prompt_tokens,
+ style=merged_style,
+ validator=validator,
+ lexer=lexer,
+ multiline=multiline,
+ **kwargs,
+ )
+ p.default_buffer.reset(Document(default))
+
+ return Question(p.app)
diff --git a/utils/questionary/questionary/py.typed b/utils/questionary/questionary/py.typed
new file mode 100644
index 00000000..e69de29b
diff --git a/utils/questionary/questionary/question.py b/utils/questionary/questionary/question.py
new file mode 100644
index 00000000..28b4a12f
--- /dev/null
+++ b/utils/questionary/questionary/question.py
@@ -0,0 +1,134 @@
+import sys
+from typing import Any
+
+import prompt_toolkit.patch_stdout
+from prompt_toolkit import Application
+
+from . import utils
+from .constants import DEFAULT_KBI_MESSAGE
+
+
+class Question:
+ """A question to be prompted.
+
+ This is an internal class. Questions should be created using the
+ predefined questions (e.g. text or password)."""
+
+ application: "Application[Any]"
+ should_skip_question: bool
+ default: Any
+
+ def __init__(self, application: "Application[Any]") -> None:
+ self.application = application
+ self.should_skip_question = False
+ self.default = None
+
+ async def ask_async(
+ self, patch_stdout: bool = False, kbi_msg: str = DEFAULT_KBI_MESSAGE
+ ) -> Any:
+ """Ask the question using asyncio and return user response.
+
+ Args:
+ 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.
+
+ Returns:
+ `Any`: The answer from the question.
+ """
+
+ try:
+ sys.stdout.flush()
+ return await self.unsafe_ask_async(patch_stdout)
+ except KeyboardInterrupt:
+ print("\n{}\n".format(kbi_msg))
+ return None
+
+ def ask(
+ self, patch_stdout: bool = False, kbi_msg: str = DEFAULT_KBI_MESSAGE
+ ) -> Any:
+ """Ask the question synchronously and return user response.
+
+ Args:
+ 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.
+
+ Returns:
+ `Any`: The answer from the question.
+ """
+
+ try:
+ return self.unsafe_ask(patch_stdout)
+ except KeyboardInterrupt:
+ print("\n{}\n".format(kbi_msg))
+ return None
+
+ def unsafe_ask(self, patch_stdout: bool = False) -> Any:
+ """Ask the question synchronously and return user response.
+
+ Does not catch keyboard interrupts.
+
+ Args:
+ patch_stdout: Ensure that the prompt renders correctly if other threads
+ are printing to stdout.
+
+ Returns:
+ `Any`: The answer from the question.
+ """
+
+ if self.should_skip_question:
+ return self.default
+
+ if patch_stdout:
+ with prompt_toolkit.patch_stdout.patch_stdout():
+ return self.application.run()
+ else:
+ return self.application.run()
+
+ def skip_if(self, condition: bool, default: Any = None) -> "Question":
+ """Skip the question if flag is set and return the default instead.
+
+ Args:
+ condition: A conditional boolean value.
+ default: The default value to return.
+
+ Returns:
+ :class:`Question`: `self`.
+ """
+
+ self.should_skip_question = condition
+ self.default = default
+ return self
+
+ async def unsafe_ask_async(self, patch_stdout: bool = False) -> Any:
+ """Ask the question using asyncio and return user response.
+
+ Does not catch keyboard interrupts.
+
+ Args:
+ patch_stdout: Ensure that the prompt renders correctly if other threads
+ are printing to stdout.
+
+ Returns:
+ `Any`: The answer from the question.
+ """
+
+ if self.should_skip_question:
+ return self.default
+
+ if not utils.ACTIVATED_ASYNC_MODE:
+ await utils.activate_prompt_toolkit_async_mode()
+
+ if patch_stdout:
+ with prompt_toolkit.patch_stdout.patch_stdout():
+ r = self.application.run_async()
+ else:
+ r = self.application.run_async()
+
+ if utils.is_prompt_toolkit_3():
+ return await r
+ else:
+ return await r.to_asyncio_future() # type: ignore[attr-defined]
diff --git a/utils/questionary/questionary/styles.py b/utils/questionary/questionary/styles.py
new file mode 100644
index 00000000..52adb101
--- /dev/null
+++ b/utils/questionary/questionary/styles.py
@@ -0,0 +1,16 @@
+
+from typing import List
+from typing import Optional
+
+import prompt_toolkit.styles
+
+from .constants import DEFAULT_STYLE
+
+
+def merge_styles_default(styles: List[Optional[prompt_toolkit.styles.Style]]):
+ """Merge a list of styles with the Questionary default style."""
+ filtered_styles: list[prompt_toolkit.styles.BaseStyle] = [DEFAULT_STYLE]
+ # prompt_toolkit's merge_styles works with ``None`` elements, but it's
+ # type-hints says it doesn't.
+ filtered_styles.extend([s for s in styles if s is not None])
+ return prompt_toolkit.styles.merge_styles(filtered_styles)
diff --git a/utils/questionary/questionary/utils.py b/utils/questionary/questionary/utils.py
new file mode 100644
index 00000000..d4fb9419
--- /dev/null
+++ b/utils/questionary/questionary/utils.py
@@ -0,0 +1,78 @@
+import inspect
+from typing import Any
+from typing import Callable
+from typing import Dict
+from typing import List
+from typing import Set
+
+ACTIVATED_ASYNC_MODE = False
+
+
+def is_prompt_toolkit_3() -> bool:
+ from prompt_toolkit import __version__ as ptk_version
+
+ return ptk_version.startswith("3.")
+
+
+def default_values_of(func: Callable[..., Any]) -> List[str]:
+ """Return all parameter names of ``func`` with a default value."""
+
+ signature = inspect.signature(func)
+ return [
+ k
+ for k, v in signature.parameters.items()
+ if v.default is not inspect.Parameter.empty
+ or v.kind != inspect.Parameter.POSITIONAL_OR_KEYWORD
+ ]
+
+
+def arguments_of(func: Callable[..., Any]) -> List[str]:
+ """Return the parameter names of the function ``func``."""
+
+ return list(inspect.signature(func).parameters.keys())
+
+
+def used_kwargs(kwargs: Dict[str, Any], func: Callable[..., Any]) -> Dict[str, Any]:
+ """Returns only the kwargs which can be used by a function.
+
+ Args:
+ kwargs: All available kwargs.
+ func: The function which should be called.
+
+ Returns:
+ Subset of kwargs which are accepted by ``func``.
+ """
+
+ possible_arguments = arguments_of(func)
+
+ return {k: v for k, v in kwargs.items() if k in possible_arguments}
+
+
+def required_arguments(func: Callable[..., Any]) -> List[str]:
+ """Return all arguments of a function that do not have a default value."""
+ defaults = default_values_of(func)
+ args = arguments_of(func)
+
+ if defaults:
+ args = args[: -len(defaults)]
+ return args # all args without default values
+
+
+def missing_arguments(func: Callable[..., Any], argdict: Dict[str, Any]) -> Set[str]:
+ """Return all arguments that are missing to call func."""
+ return set(required_arguments(func)) - set(argdict.keys())
+
+
+async def activate_prompt_toolkit_async_mode() -> None:
+ """Configure prompt toolkit to use the asyncio event loop.
+
+ Needs to be async, so we use the right event loop in py 3.5"""
+ global ACTIVATED_ASYNC_MODE
+
+ if not is_prompt_toolkit_3():
+ # Tell prompt_toolkit to use asyncio for the event loop.
+ import prompt_toolkit as pt
+
+ pt.eventloop.use_asyncio_event_loop() # type: ignore[attr-defined]
+
+ ACTIVATED_ASYNC_MODE = True
diff --git a/utils/questionary/questionary/version.py b/utils/questionary/questionary/version.py
new file mode 100644
index 00000000..cf52fbca
--- /dev/null
+++ b/utils/questionary/questionary/version.py
@@ -0,0 +1 @@
+__version__ = "2.0.2-beta"