Initial commit: Tamigo CLI with Gitea Actions and global installation support
This commit is contained in:
56
venv/lib/python3.12/site-packages/questionary/__init__.py
Normal file
56
venv/lib/python3.12/site-packages/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",
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
57
venv/lib/python3.12/site-packages/questionary/constants.py
Normal file
57
venv/lib/python3.12/site-packages/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
venv/lib/python3.12/site-packages/questionary/form.py
Normal file
117
venv/lib/python3.12/site-packages/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
venv/lib/python3.12/site-packages/questionary/prompt.py
Normal file
234
venv/lib/python3.12/site-packages/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
|
||||
@@ -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)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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)
|
||||
@@ -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
venv/lib/python3.12/site-packages/questionary/prompts/common.py
Normal file
670
venv/lib/python3.12/site-packages/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
venv/lib/python3.12/site-packages/questionary/prompts/confirm.py
Normal file
133
venv/lib/python3.12/site-packages/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
|
||||
)
|
||||
@@ -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
venv/lib/python3.12/site-packages/questionary/prompts/path.py
Normal file
243
venv/lib/python3.12/site-packages/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)
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
venv/lib/python3.12/site-packages/questionary/prompts/select.py
Normal file
283
venv/lib/python3.12/site-packages/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
venv/lib/python3.12/site-packages/questionary/prompts/text.py
Normal file
101
venv/lib/python3.12/site-packages/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)
|
||||
134
venv/lib/python3.12/site-packages/questionary/question.py
Normal file
134
venv/lib/python3.12/site-packages/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
venv/lib/python3.12/site-packages/questionary/styles.py
Normal file
15
venv/lib/python3.12/site-packages/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
venv/lib/python3.12/site-packages/questionary/utils.py
Normal file
78
venv/lib/python3.12/site-packages/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
venv/lib/python3.12/site-packages/questionary/version.py
Normal file
1
venv/lib/python3.12/site-packages/questionary/version.py
Normal file
@@ -0,0 +1 @@
|
||||
__version__ = "2.1.1"
|
||||
Reference in New Issue
Block a user