Init
im going to bed -=-
This commit is contained in:
56
lib/questionary/__init__.py
Normal file
56
lib/questionary/__init__.py
Normal file
@@ -0,0 +1,56 @@
|
||||
# noinspection PyUnresolvedReferences
|
||||
from prompt_toolkit.styles import Style
|
||||
from prompt_toolkit.validation import ValidationError
|
||||
from prompt_toolkit.validation import Validator
|
||||
|
||||
import questionary.version
|
||||
from questionary.form import Form
|
||||
from questionary.form import FormField
|
||||
from questionary.form import form
|
||||
from questionary.prompt import prompt
|
||||
from questionary.prompt import unsafe_prompt
|
||||
|
||||
# import the shortcuts to create single question prompts
|
||||
from questionary.prompts.autocomplete import autocomplete
|
||||
from questionary.prompts.checkbox import checkbox
|
||||
from questionary.prompts.common import Choice
|
||||
from questionary.prompts.common import Separator
|
||||
from questionary.prompts.common import print_formatted_text as print
|
||||
from questionary.prompts.confirm import confirm
|
||||
from questionary.prompts.password import password
|
||||
from questionary.prompts.path import path
|
||||
from questionary.prompts.press_any_key_to_continue import press_any_key_to_continue
|
||||
from questionary.prompts.rawselect import rawselect
|
||||
from questionary.prompts.select import select
|
||||
from questionary.prompts.text import text
|
||||
from questionary.question import Question
|
||||
|
||||
__version__ = questionary.version.__version__
|
||||
|
||||
__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",
|
||||
]
|
||||
BIN
lib/questionary/__pycache__/__init__.cpython-314.pyc
Normal file
BIN
lib/questionary/__pycache__/__init__.cpython-314.pyc
Normal file
Binary file not shown.
BIN
lib/questionary/__pycache__/constants.cpython-314.pyc
Normal file
BIN
lib/questionary/__pycache__/constants.cpython-314.pyc
Normal file
Binary file not shown.
BIN
lib/questionary/__pycache__/form.cpython-314.pyc
Normal file
BIN
lib/questionary/__pycache__/form.cpython-314.pyc
Normal file
Binary file not shown.
BIN
lib/questionary/__pycache__/prompt.cpython-314.pyc
Normal file
BIN
lib/questionary/__pycache__/prompt.cpython-314.pyc
Normal file
Binary file not shown.
BIN
lib/questionary/__pycache__/question.cpython-314.pyc
Normal file
BIN
lib/questionary/__pycache__/question.cpython-314.pyc
Normal file
Binary file not shown.
BIN
lib/questionary/__pycache__/styles.cpython-314.pyc
Normal file
BIN
lib/questionary/__pycache__/styles.cpython-314.pyc
Normal file
Binary file not shown.
BIN
lib/questionary/__pycache__/utils.cpython-314.pyc
Normal file
BIN
lib/questionary/__pycache__/utils.cpython-314.pyc
Normal file
Binary file not shown.
BIN
lib/questionary/__pycache__/version.cpython-314.pyc
Normal file
BIN
lib/questionary/__pycache__/version.cpython-314.pyc
Normal file
Binary file not shown.
57
lib/questionary/constants.py
Normal file
57
lib/questionary/constants.py
Normal file
@@ -0,0 +1,57 @@
|
||||
from questionary 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 = "\nCancelled by user\n"
|
||||
|
||||
# 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
|
||||
(
|
||||
"search_success",
|
||||
"noinherit fg:#00FF00 bold",
|
||||
), # submitted answer text behind the question
|
||||
(
|
||||
"search_none",
|
||||
"noinherit fg:#FF0000 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
|
||||
]
|
||||
)
|
||||
117
lib/questionary/form.py
Normal file
117
lib/questionary/form.py
Normal file
@@ -0,0 +1,117 @@
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
from typing import NamedTuple
|
||||
from typing import Sequence
|
||||
|
||||
from questionary.constants import DEFAULT_KBI_MESSAGE
|
||||
from questionary.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(kbi_msg)
|
||||
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(kbi_msg)
|
||||
return {}
|
||||
234
lib/questionary/prompt.py
Normal file
234
lib/questionary/prompt.py
Normal file
@@ -0,0 +1,234 @@
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
from typing import Iterable
|
||||
from typing import Mapping
|
||||
from typing import Optional
|
||||
from typing import Union
|
||||
|
||||
from prompt_toolkit.output import ColorDepth
|
||||
|
||||
from questionary import utils
|
||||
from questionary.constants import DEFAULT_KBI_MESSAGE
|
||||
from questionary.prompts import AVAILABLE_PROMPTS
|
||||
from questionary.prompts import prompt_by_name
|
||||
from questionary.prompts.common import print_formatted_text
|
||||
|
||||
|
||||
class PromptParameterException(ValueError):
|
||||
"""Received a prompt with a missing parameter."""
|
||||
|
||||
def __init__(self, message: str, errors: Optional[BaseException] = None) -> None:
|
||||
# Call the base class constructor with the parameters it needs
|
||||
super().__init__(f"You must provide a `{message}` value", errors)
|
||||
|
||||
|
||||
def prompt(
|
||||
questions: Union[Dict[str, Any], Iterable[Mapping[str, Any]]],
|
||||
answers: Optional[Mapping[str, Any]] = None,
|
||||
patch_stdout: bool = False,
|
||||
true_color: bool = False,
|
||||
kbi_msg: str = DEFAULT_KBI_MESSAGE,
|
||||
**kwargs: Any,
|
||||
) -> Dict[str, Any]:
|
||||
"""Prompt the user for input on all the questions.
|
||||
|
||||
Catches keyboard interrupts and prints a message.
|
||||
|
||||
See :func:`unsafe_prompt` for possible question configurations.
|
||||
|
||||
Args:
|
||||
questions: A list of question configs representing questions to
|
||||
ask. A question config may have the following options:
|
||||
|
||||
* type - The type of question.
|
||||
* name - An ID for the question (to identify it in the answers :obj:`dict`).
|
||||
|
||||
* when - Callable to conditionally show the question. This function
|
||||
takes a :obj:`dict` representing the current answers.
|
||||
|
||||
* filter - Function that the answer is passed to. The return value of this
|
||||
function is saved as the answer.
|
||||
|
||||
Additional options correspond to the parameter names for
|
||||
particular question types.
|
||||
|
||||
answers: Default answers.
|
||||
|
||||
patch_stdout: Ensure that the prompt renders correctly if other threads
|
||||
are printing to stdout.
|
||||
|
||||
kbi_msg: The message to be printed on a keyboard interrupt.
|
||||
true_color: Use true color output.
|
||||
|
||||
color_depth: Color depth to use. If ``true_color`` is set to true then this
|
||||
value is ignored.
|
||||
|
||||
type: Default ``type`` value to use in question config.
|
||||
filter: Default ``filter`` value to use in question config.
|
||||
name: Default ``name`` value to use in question config.
|
||||
when: Default ``when`` value to use in question config.
|
||||
default: Default ``default`` value to use in question config.
|
||||
kwargs: Additional options passed to every question.
|
||||
|
||||
Returns:
|
||||
Dictionary of question answers.
|
||||
"""
|
||||
|
||||
try:
|
||||
return unsafe_prompt(questions, answers, patch_stdout, true_color, **kwargs)
|
||||
except KeyboardInterrupt:
|
||||
print(kbi_msg)
|
||||
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
|
||||
29
lib/questionary/prompts/__init__.py
Normal file
29
lib/questionary/prompts/__init__.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from questionary.prompts import autocomplete
|
||||
from questionary.prompts import checkbox
|
||||
from questionary.prompts import confirm
|
||||
from questionary.prompts import password
|
||||
from questionary.prompts import path
|
||||
from questionary.prompts import press_any_key_to_continue
|
||||
from questionary.prompts import rawselect
|
||||
from questionary.prompts import select
|
||||
from questionary.prompts 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)
|
||||
BIN
lib/questionary/prompts/__pycache__/__init__.cpython-314.pyc
Normal file
BIN
lib/questionary/prompts/__pycache__/__init__.cpython-314.pyc
Normal file
Binary file not shown.
BIN
lib/questionary/prompts/__pycache__/autocomplete.cpython-314.pyc
Normal file
BIN
lib/questionary/prompts/__pycache__/autocomplete.cpython-314.pyc
Normal file
Binary file not shown.
BIN
lib/questionary/prompts/__pycache__/checkbox.cpython-314.pyc
Normal file
BIN
lib/questionary/prompts/__pycache__/checkbox.cpython-314.pyc
Normal file
Binary file not shown.
BIN
lib/questionary/prompts/__pycache__/common.cpython-314.pyc
Normal file
BIN
lib/questionary/prompts/__pycache__/common.cpython-314.pyc
Normal file
Binary file not shown.
BIN
lib/questionary/prompts/__pycache__/confirm.cpython-314.pyc
Normal file
BIN
lib/questionary/prompts/__pycache__/confirm.cpython-314.pyc
Normal file
Binary file not shown.
BIN
lib/questionary/prompts/__pycache__/password.cpython-314.pyc
Normal file
BIN
lib/questionary/prompts/__pycache__/password.cpython-314.pyc
Normal file
Binary file not shown.
BIN
lib/questionary/prompts/__pycache__/path.cpython-314.pyc
Normal file
BIN
lib/questionary/prompts/__pycache__/path.cpython-314.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
lib/questionary/prompts/__pycache__/rawselect.cpython-314.pyc
Normal file
BIN
lib/questionary/prompts/__pycache__/rawselect.cpython-314.pyc
Normal file
Binary file not shown.
BIN
lib/questionary/prompts/__pycache__/select.cpython-314.pyc
Normal file
BIN
lib/questionary/prompts/__pycache__/select.cpython-314.pyc
Normal file
Binary file not shown.
BIN
lib/questionary/prompts/__pycache__/text.cpython-314.pyc
Normal file
BIN
lib/questionary/prompts/__pycache__/text.cpython-314.pyc
Normal file
Binary file not shown.
214
lib/questionary/prompts/autocomplete.py
Normal file
214
lib/questionary/prompts/autocomplete.py
Normal file
@@ -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 questionary.constants import DEFAULT_QUESTION_PREFIX
|
||||
from questionary.prompts.common import build_validator
|
||||
from questionary.question import Question
|
||||
from questionary.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("{}<b><u>{}</u></b>{}").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 species',
|
||||
... choices=[
|
||||
... 'Camponotus pennsylvanicus',
|
||||
... 'Linepithema humile',
|
||||
... 'Eciton burchellii',
|
||||
... "Atta colombica",
|
||||
... 'Polyergus lucidus',
|
||||
... 'Polyergus rufescens',
|
||||
... ]).ask()
|
||||
? Choose ant species 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("<text>{}</text>").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)
|
||||
327
lib/questionary/prompts/checkbox.py
Normal file
327
lib/questionary/prompts/checkbox.py
Normal file
@@ -0,0 +1,327 @@
|
||||
import string
|
||||
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 questionary import utils
|
||||
from questionary.constants import DEFAULT_QUESTION_PREFIX
|
||||
from questionary.constants import DEFAULT_SELECTED_POINTER
|
||||
from questionary.constants import INVALID_INPUT
|
||||
from questionary.prompts import common
|
||||
from questionary.prompts.common import Choice
|
||||
from questionary.prompts.common import InquirerControl
|
||||
from questionary.prompts.common import Separator
|
||||
from questionary.question import Question
|
||||
from questionary.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,
|
||||
use_search_filter: Union[str, bool, None] = False,
|
||||
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.
|
||||
|
||||
use_search_filter: Flag to enable search filtering. Typing some string will
|
||||
filter the choices to keep only the ones that contain the
|
||||
search string.
|
||||
Note that activating this option disables "vi-like"
|
||||
navigation as "j" and "k" can be part of a prefix and
|
||||
therefore cannot be used for navigation
|
||||
|
||||
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."
|
||||
)
|
||||
|
||||
if use_jk_keys and use_search_filter:
|
||||
raise ValueError(
|
||||
"Cannot use j/k keys with prefix filter search, since j/k can be part of the prefix."
|
||||
)
|
||||
|
||||
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, "
|
||||
"<space> to select, "
|
||||
f"<{'ctrl-a' if use_search_filter else 'a'}> to toggle, "
|
||||
f"<{'ctrl-a' if use_search_filter else 'i'}> to invert"
|
||||
f"{', type to filter' if use_search_filter else ''})",
|
||||
)
|
||||
)
|
||||
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(Keys.ControlI if use_search_filter else "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(Keys.ControlA if use_search_filter else "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_search_filter:
|
||||
|
||||
def search_filter(event):
|
||||
ic.add_search_character(event.key_sequence[0].key)
|
||||
|
||||
for character in string.printable:
|
||||
if character in string.whitespace:
|
||||
continue
|
||||
bindings.add(character, eager=True)(search_filter)
|
||||
bindings.add(Keys.Backspace, eager=True)(search_filter)
|
||||
|
||||
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__),
|
||||
)
|
||||
)
|
||||
670
lib/questionary/prompts/common.py
Normal file
670
lib/questionary/prompts/common.py
Normal file
@@ -0,0 +1,670 @@
|
||||
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.keys import Keys
|
||||
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.layout.controls import BufferControl
|
||||
from prompt_toolkit.layout.dimension import LayoutDimension
|
||||
from prompt_toolkit.styles import Style
|
||||
from prompt_toolkit.validation import ValidationError
|
||||
from prompt_toolkit.validation import Validator
|
||||
|
||||
from questionary.constants import DEFAULT_SELECTED_POINTER
|
||||
from questionary.constants import DEFAULT_STYLE
|
||||
from questionary.constants import INDICATOR_SELECTED
|
||||
from questionary.constants import INDICATOR_UNSELECTED
|
||||
from questionary.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[Union[str, bool]]
|
||||
|
||||
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.shortcut_key = shortcut_key
|
||||
# self.auto_shortcut is set by the self.shortcut_key setter
|
||||
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
|
||||
|
||||
@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),
|
||||
)
|
||||
|
||||
@property
|
||||
def shortcut_key(self) -> Optional[Union[str, bool]]:
|
||||
"""A shortcut key for the choice"""
|
||||
return self.__shortcut_key
|
||||
|
||||
@shortcut_key.setter
|
||||
def shortcut_key(self, key: Optional[Union[str, bool]]):
|
||||
if key is not None:
|
||||
if isinstance(key, bool):
|
||||
self.__auto_shortcut = key
|
||||
self.__shortcut_key = None
|
||||
else:
|
||||
self.__shortcut_key = str(key)
|
||||
self.__auto_shortcut = False
|
||||
else:
|
||||
self.__shortcut_key = None
|
||||
self.__auto_shortcut = True
|
||||
|
||||
@shortcut_key.deleter
|
||||
def shortcut_key(self):
|
||||
self.__shortcut_key = None
|
||||
self.__auto_shortcut = True
|
||||
|
||||
def get_shortcut_title(self):
|
||||
if self.shortcut_key is None:
|
||||
return "-) "
|
||||
else:
|
||||
return "{}) ".format(self.shortcut_key)
|
||||
|
||||
@property
|
||||
def auto_shortcut(self) -> bool:
|
||||
"""Whether to assign a shortcut key to the choice
|
||||
|
||||
Keys are assigned starting with numbers and proceeding
|
||||
through the ASCII alphabet.
|
||||
"""
|
||||
return self.__auto_shortcut
|
||||
|
||||
@auto_shortcut.setter
|
||||
def auto_shortcut(self, should_assign: bool):
|
||||
self.__auto_shortcut = should_assign
|
||||
if self.__auto_shortcut:
|
||||
self.__shortcut_key = None
|
||||
|
||||
@auto_shortcut.deleter
|
||||
def auto_shortcut(self):
|
||||
self.__auto_shortcut = False
|
||||
|
||||
|
||||
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]
|
||||
search_filter: Union[str, None] = None
|
||||
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.found_in_search = False
|
||||
|
||||
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 filtered_choices(self):
|
||||
if not self.search_filter:
|
||||
return self.choices
|
||||
filtered = [
|
||||
c for c in self.choices if self.search_filter.lower() in c.title.lower()
|
||||
]
|
||||
self.found_in_search = len(filtered) > 0
|
||||
return filtered if self.found_in_search else self.choices
|
||||
|
||||
@property
|
||||
def choice_count(self) -> int:
|
||||
return len(self.filtered_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.filtered_choices):
|
||||
append(i, c)
|
||||
|
||||
current = self.get_pointed_at()
|
||||
|
||||
if self.show_selected:
|
||||
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)))
|
||||
|
||||
show_description = self.show_description and current.description is not None
|
||||
if show_description:
|
||||
tokens.append(
|
||||
("class:text", " Description: {}".format(current.description))
|
||||
)
|
||||
|
||||
if not (self.show_selected or show_description):
|
||||
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.filtered_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 add_search_character(self, char: Keys) -> None:
|
||||
"""Adds a character to the search filter"""
|
||||
if char == Keys.Backspace:
|
||||
self.remove_search_character()
|
||||
else:
|
||||
if self.search_filter is None:
|
||||
self.search_filter = str(char)
|
||||
else:
|
||||
self.search_filter += str(char)
|
||||
|
||||
# Make sure that the selection is in the bounds of the filtered list
|
||||
self.pointed_at = 0
|
||||
|
||||
def remove_search_character(self) -> None:
|
||||
if self.search_filter and len(self.search_filter) > 1:
|
||||
self.search_filter = self.search_filter[:-1]
|
||||
else:
|
||||
self.search_filter = None
|
||||
|
||||
def get_search_string_tokens(self):
|
||||
if self.search_filter is None:
|
||||
return None
|
||||
|
||||
return [
|
||||
("", "\n"),
|
||||
("class:question-mark", "/ "),
|
||||
(
|
||||
"class:search_success" if self.found_in_search else "class:search_none",
|
||||
self.search_filter,
|
||||
),
|
||||
("class:question-mark", "..."),
|
||||
]
|
||||
|
||||
|
||||
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_buffer_window: Window = next(
|
||||
win
|
||||
for win in ps.layout.find_all_windows()
|
||||
if isinstance(win.content, BufferControl)
|
||||
and win.content.buffer.name == "DEFAULT_BUFFER"
|
||||
)
|
||||
|
||||
# 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)
|
||||
|
||||
@Condition
|
||||
def has_search_string():
|
||||
return ic.get_search_string_tokens() is not None
|
||||
|
||||
validation_prompt: PromptSession = PromptSession(
|
||||
bottom_toolbar=lambda: ic.error_message, **kwargs
|
||||
)
|
||||
|
||||
return Layout(
|
||||
HSplit(
|
||||
[
|
||||
ps.layout.container,
|
||||
ConditionalContainer(Window(ic), filter=~IsDone()),
|
||||
ConditionalContainer(
|
||||
Window(
|
||||
height=LayoutDimension.exact(2),
|
||||
content=FormattedTextControl(ic.get_search_string_tokens),
|
||||
),
|
||||
filter=has_search_string & ~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 <prompt_toolkit:styling>`.
|
||||
"""
|
||||
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)
|
||||
133
lib/questionary/prompts/confirm.py
Normal file
133
lib/questionary/prompts/confirm.py
Normal file
@@ -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 questionary.constants import DEFAULT_QUESTION_PREFIX
|
||||
from questionary.constants import NO
|
||||
from questionary.constants import NO_OR_YES
|
||||
from questionary.constants import YES
|
||||
from questionary.constants import YES_OR_NO
|
||||
from questionary.question import Question
|
||||
from questionary.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
|
||||
)
|
||||
61
lib/questionary/prompts/password.py
Normal file
61
lib/questionary/prompts/password.py
Normal file
@@ -0,0 +1,61 @@
|
||||
from typing import Any
|
||||
from typing import Optional
|
||||
|
||||
from questionary import Style
|
||||
from questionary.constants import DEFAULT_QUESTION_PREFIX
|
||||
from questionary.prompts import text
|
||||
from questionary.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
|
||||
)
|
||||
243
lib/questionary/prompts/path.py
Normal file
243
lib/questionary/prompts/path.py
Normal file
@@ -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 questionary.constants import DEFAULT_QUESTION_PREFIX
|
||||
from questionary.prompts.common import build_validator
|
||||
from questionary.question import Question
|
||||
from questionary.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 <https://python-prompt-toolkit.readthedocs.io/en/master/pages/asking_for_input.html#a-custom-completer>`_.
|
||||
|
||||
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)
|
||||
61
lib/questionary/prompts/press_any_key_to_continue.py
Normal file
61
lib/questionary/prompts/press_any_key_to_continue.py
Normal file
@@ -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 questionary.question import Question
|
||||
from questionary.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
|
||||
)
|
||||
79
lib/questionary/prompts/rawselect.py
Normal file
79
lib/questionary/prompts/rawselect.py
Normal file
@@ -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 questionary.constants import DEFAULT_QUESTION_PREFIX
|
||||
from questionary.constants import DEFAULT_SELECTED_POINTER
|
||||
from questionary.prompts import select
|
||||
from questionary.prompts.common import Choice
|
||||
from questionary.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,
|
||||
)
|
||||
283
lib/questionary/prompts/select.py
Normal file
283
lib/questionary/prompts/select.py
Normal file
@@ -0,0 +1,283 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import string
|
||||
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 questionary import utils
|
||||
from questionary.constants import DEFAULT_QUESTION_PREFIX
|
||||
from questionary.constants import DEFAULT_SELECTED_POINTER
|
||||
from questionary.prompts import common
|
||||
from questionary.prompts.common import Choice
|
||||
from questionary.prompts.common import InquirerControl
|
||||
from questionary.prompts.common import Separator
|
||||
from questionary.question import Question
|
||||
from questionary.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,
|
||||
use_search_filter: bool = False,
|
||||
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.
|
||||
|
||||
use_search_filter: Flag to enable search filtering. Typing some string will
|
||||
filter the choices to keep only the ones that contain the
|
||||
search string.
|
||||
Note that activating this option disables "vi-like"
|
||||
navigation as "j" and "k" can be part of a prefix and
|
||||
therefore cannot be used for navigation
|
||||
|
||||
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_jk_keys and use_search_filter:
|
||||
raise ValueError(
|
||||
"Cannot use j/k keys with prefix filter search, since j/k can be part of the prefix."
|
||||
)
|
||||
|
||||
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:
|
||||
real_len_of_choices = sum(1 for c in choices if not isinstance(c, Separator))
|
||||
if real_len_of_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), real_len_of_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 = f"(Use shortcuts or arrow keys{', type to filter' if use_search_filter else ''})"
|
||||
elif use_shortcuts and not use_arrow_keys:
|
||||
instruction_msg = f"(Use shortcuts{', type to filter' if use_search_filter else ''})"
|
||||
else:
|
||||
instruction_msg = f"(Use arrow keys{', type to filter' if use_search_filter else ''})"
|
||||
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 or c.disabled:
|
||||
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_search_filter:
|
||||
|
||||
def search_filter(event):
|
||||
ic.add_search_character(event.key_sequence[0].key)
|
||||
|
||||
for character in string.printable:
|
||||
bindings.add(character, eager=True)(search_filter)
|
||||
bindings.add(Keys.Backspace, eager=True)(search_filter)
|
||||
|
||||
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__),
|
||||
)
|
||||
)
|
||||
101
lib/questionary/prompts/text.py
Normal file
101
lib/questionary/prompts/text.py
Normal file
@@ -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 questionary.constants import DEFAULT_QUESTION_PREFIX
|
||||
from questionary.constants import INSTRUCTION_MULTILINE
|
||||
from questionary.prompts.common import build_validator
|
||||
from questionary.question import Question
|
||||
from questionary.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)
|
||||
0
lib/questionary/py.typed
Normal file
0
lib/questionary/py.typed
Normal file
134
lib/questionary/question.py
Normal file
134
lib/questionary/question.py
Normal file
@@ -0,0 +1,134 @@
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
import prompt_toolkit.patch_stdout
|
||||
from prompt_toolkit import Application
|
||||
|
||||
from questionary import utils
|
||||
from questionary.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("{}".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("{}".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]
|
||||
15
lib/questionary/styles.py
Normal file
15
lib/questionary/styles.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
|
||||
import prompt_toolkit.styles
|
||||
|
||||
from questionary.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)
|
||||
78
lib/questionary/utils.py
Normal file
78
lib/questionary/utils.py
Normal file
@@ -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
|
||||
1
lib/questionary/version.py
Normal file
1
lib/questionary/version.py
Normal file
@@ -0,0 +1 @@
|
||||
__version__ = "2.1.1"
|
||||
Reference in New Issue
Block a user