mirror of
https://github.com/Textualize/rich.git
synced 2025-08-04 10:08:40 +00:00
prompt update
This commit is contained in:
parent
a5a40b6300
commit
73de62b9fc
7 changed files with 172 additions and 30 deletions
|
@ -7,3 +7,4 @@ exclude_lines =
|
|||
pragma: no cover
|
||||
if TYPE_CHECKING:
|
||||
if __name__ == "__main__":
|
||||
@overload
|
||||
|
|
|
@ -9,7 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
### Added
|
||||
|
||||
- Added input_file parameter to Console.input
|
||||
- Added password parameter to Console.input
|
||||
- Added description parameter to Progress.update
|
||||
- Added rich.prompt
|
||||
|
||||
## [3.3.2] - 2020-07-14
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ Reference
|
|||
reference/padding.rst
|
||||
reference/panel.rst
|
||||
reference/progress.rst
|
||||
reference/prompt.rst
|
||||
reference/rule.rst
|
||||
reference/segment.rst
|
||||
reference/style.rst
|
||||
|
|
5
docs/source/reference/prompt.rst
Normal file
5
docs/source/reference/prompt.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
rich.progress
|
||||
=============
|
||||
|
||||
.. automodule:: rich.prompt
|
||||
:members:
|
|
@ -1,5 +1,7 @@
|
|||
from collections.abc import Mapping, Sequence
|
||||
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass, field, replace
|
||||
from functools import wraps
|
||||
from getpass import getpass
|
||||
|
@ -21,6 +23,7 @@ from typing import (
|
|||
Iterable,
|
||||
List,
|
||||
Optional,
|
||||
TextIO,
|
||||
TYPE_CHECKING,
|
||||
NamedTuple,
|
||||
Union,
|
||||
|
@ -238,9 +241,10 @@ class ConsoleThreadLocals(threading.local):
|
|||
buffer_index: int = 0
|
||||
|
||||
|
||||
class RenderHook:
|
||||
class RenderHook(ABC):
|
||||
"""Provides hooks in to the render process."""
|
||||
|
||||
@abstractmethod
|
||||
def process_renderables(
|
||||
self, renderables: List[ConsoleRenderable]
|
||||
) -> List[ConsoleRenderable]:
|
||||
|
@ -254,7 +258,6 @@ class RenderHook:
|
|||
Returns:
|
||||
List[ConsoleRenderable]: A replacement list of renderables.
|
||||
"""
|
||||
return renderables
|
||||
|
||||
|
||||
_windows_console_features: Optional["WindowsConsoleFeatures"] = None
|
||||
|
@ -996,6 +999,7 @@ class Console:
|
|||
markup: bool = True,
|
||||
emoji: bool = True,
|
||||
password: bool = False,
|
||||
stream: TextIO = None,
|
||||
) -> str:
|
||||
"""Displays a prompt and waits for input from the user. The prompt may contain color / style.
|
||||
|
||||
|
@ -1004,13 +1008,18 @@ class Console:
|
|||
markup (bool, optional): Enable console markup (requires a str prompt). Defaults to True.
|
||||
emoji (bool, optional): Enable emoji (requires a str prompt). Defaults to True.
|
||||
password: (bool, optional): Hide typed text. Defaults to False.
|
||||
stream: (IO[str], optional): Optional file to read input from (rather than stdin). Defaults to None.
|
||||
|
||||
Returns:
|
||||
str: Text read from stdin.
|
||||
"""
|
||||
if prompt:
|
||||
self.print(prompt, markup=markup, emoji=emoji, end="")
|
||||
result = getpass("") if password else input()
|
||||
result = (
|
||||
getpass("", stream=stream)
|
||||
if password
|
||||
else (stream.readline() if stream else input())
|
||||
)
|
||||
return result
|
||||
|
||||
def export_text(self, *, clear: bool = True, styles: bool = False) -> str:
|
||||
|
|
107
rich/prompt.py
107
rich/prompt.py
|
@ -1,9 +1,11 @@
|
|||
from typing import (
|
||||
Any,
|
||||
Generic,
|
||||
IO,
|
||||
List,
|
||||
overload,
|
||||
Optional,
|
||||
TextIO,
|
||||
TypeVar,
|
||||
Union,
|
||||
)
|
||||
|
@ -25,7 +27,7 @@ class InvalidResponse(PromptError):
|
|||
"""Exception to indicate a response was invalid.
|
||||
|
||||
Args:
|
||||
message (str): Error message.
|
||||
message (str): Error message.
|
||||
"""
|
||||
|
||||
def __init__(self, message: str) -> None:
|
||||
|
@ -36,7 +38,8 @@ class InvalidResponse(PromptError):
|
|||
|
||||
|
||||
class PromptBase(Generic[PromptType]):
|
||||
"""Ask the user for input until a valid response is received.
|
||||
"""Ask the user for input until a valid response is received. This is the base class, see one of
|
||||
the concrete classes for examples.
|
||||
|
||||
Args:
|
||||
prompt (TextType, optional): Prompt text. Defaults to "".
|
||||
|
@ -91,6 +94,7 @@ class PromptBase(Generic[PromptType]):
|
|||
show_default: bool = True,
|
||||
show_choices: bool = True,
|
||||
default: DefaultType,
|
||||
stream: TextIO = None,
|
||||
) -> Union[DefaultType, PromptType]:
|
||||
...
|
||||
|
||||
|
@ -105,6 +109,7 @@ class PromptBase(Generic[PromptType]):
|
|||
choices: List[str] = None,
|
||||
show_default: bool = True,
|
||||
show_choices: bool = True,
|
||||
stream: TextIO = None,
|
||||
) -> PromptType:
|
||||
...
|
||||
|
||||
|
@ -118,8 +123,23 @@ class PromptBase(Generic[PromptType]):
|
|||
choices: List[str] = None,
|
||||
show_default: bool = True,
|
||||
show_choices: bool = True,
|
||||
default: DefaultType = ...,
|
||||
) -> PromptType:
|
||||
default: Any = ...,
|
||||
stream: TextIO = None,
|
||||
) -> Any:
|
||||
"""Shortcut to run prompt loop and return the result.
|
||||
|
||||
Example:
|
||||
>>> filename = Promot.ask("Enter a filename")
|
||||
|
||||
Args:
|
||||
prompt (TextType, optional): Prompt text. Defaults to "".
|
||||
console (Console, optional): A Console instance or None to use global console. Defaults to None.
|
||||
password (bool, optional): Enable password input. Defaults to False.
|
||||
choices (List[str], optional): A list of valid choices. Defaults to None.
|
||||
show_default (bool, optional): Show default in prompt. Defaults to True.
|
||||
show_choices (bool, optional): Show choices in prompt. Defaults to True.
|
||||
stream (TextIO, optional): Optional text file open for readding to get input. Defaults to None.
|
||||
"""
|
||||
_prompt = cls(
|
||||
prompt,
|
||||
console=console,
|
||||
|
@ -128,7 +148,7 @@ class PromptBase(Generic[PromptType]):
|
|||
show_default=show_default,
|
||||
show_choices=show_choices,
|
||||
)
|
||||
return _prompt(default=default)
|
||||
return _prompt(default=default, stream=stream)
|
||||
|
||||
def make_prompt(self, default: DefaultType) -> Text:
|
||||
"""Make prompt text.
|
||||
|
@ -141,26 +161,29 @@ class PromptBase(Generic[PromptType]):
|
|||
"""
|
||||
prompt = self.prompt.copy()
|
||||
prompt.end = ""
|
||||
prompt.append(self.prompt_suffix)
|
||||
|
||||
if self.show_choices and self.choices:
|
||||
_choices = "/".join(self.choices)
|
||||
choices = f"[{_choices}]"
|
||||
prompt.append(choices, "prompt.choices")
|
||||
prompt.append(" ")
|
||||
prompt.append(choices, "prompt.choices")
|
||||
|
||||
if (
|
||||
default != ...
|
||||
and self.show_default
|
||||
and isinstance(default, (str, self.response_type))
|
||||
):
|
||||
prompt.append(f"({default})", "prompt.default")
|
||||
prompt.append(" ")
|
||||
prompt.append(f"({default})", "prompt.default")
|
||||
|
||||
prompt.append(self.prompt_suffix)
|
||||
|
||||
return prompt
|
||||
|
||||
@classmethod
|
||||
def get_input(cls, console: Console, prompt: TextType, password: bool) -> str:
|
||||
def get_input(
|
||||
cls, console: Console, prompt: TextType, password: bool, stream: TextIO = None,
|
||||
) -> str:
|
||||
"""Get input from user.
|
||||
|
||||
Args:
|
||||
|
@ -171,7 +194,7 @@ class PromptBase(Generic[PromptType]):
|
|||
Returns:
|
||||
str: String from user.
|
||||
"""
|
||||
return console.input(prompt, password=password)
|
||||
return console.input(prompt, password=password, stream=stream)
|
||||
|
||||
def check_choice(self, value: str) -> bool:
|
||||
"""Check value is in the list of valid choices.
|
||||
|
@ -208,7 +231,7 @@ class PromptBase(Generic[PromptType]):
|
|||
|
||||
return return_value
|
||||
|
||||
def on_validate_error(self, value: str, error: InvalidResponse):
|
||||
def on_validate_error(self, value: str, error: InvalidResponse) -> None:
|
||||
"""Called to handle validation error.
|
||||
|
||||
Args:
|
||||
|
@ -217,15 +240,20 @@ class PromptBase(Generic[PromptType]):
|
|||
"""
|
||||
self.console.print(error)
|
||||
|
||||
def pre_prompt(self) -> None:
|
||||
"""Hook to display something before the prompt."""
|
||||
|
||||
@overload
|
||||
def __call__(self) -> PromptType:
|
||||
def __call__(self, *, stream: TextIO = None) -> PromptType:
|
||||
...
|
||||
|
||||
@overload
|
||||
def __call__(self, *, default: DefaultType) -> Union[PromptType, DefaultType]:
|
||||
def __call__(
|
||||
self, *, default: DefaultType, stream: TextIO = None
|
||||
) -> Union[PromptType, DefaultType]:
|
||||
...
|
||||
|
||||
def __call__(self, *, default: Any = ...) -> Any:
|
||||
def __call__(self, *, default: Any = ..., stream: TextIO = None) -> Any:
|
||||
"""Run the prompt loop.
|
||||
|
||||
Args:
|
||||
|
@ -235,8 +263,9 @@ class PromptBase(Generic[PromptType]):
|
|||
PromptType: Processed value.
|
||||
"""
|
||||
while True:
|
||||
self.pre_prompt()
|
||||
prompt = self.make_prompt(default)
|
||||
value = self.get_input(self.console, prompt, self.password)
|
||||
value = self.get_input(self.console, prompt, self.password, stream=stream)
|
||||
if value == "" and default != ...:
|
||||
return default
|
||||
try:
|
||||
|
@ -249,34 +278,55 @@ class PromptBase(Generic[PromptType]):
|
|||
|
||||
|
||||
class Prompt(PromptBase[str]):
|
||||
"""A prompt that returns a str."""
|
||||
"""A prompt that returns a str.
|
||||
|
||||
Example:
|
||||
>>> name = Prompt.ask("Enter your name")
|
||||
|
||||
|
||||
"""
|
||||
|
||||
response_type = str
|
||||
|
||||
|
||||
class IntPrompt(PromptBase[int]):
|
||||
"""A prompt that returns an integer."""
|
||||
"""A prompt that returns an integer.
|
||||
|
||||
Example:
|
||||
>>> burrito_count = IntPrompt.ask("How many burritos do you want to order", prompt_suffix="? ")
|
||||
|
||||
"""
|
||||
|
||||
response_type = int
|
||||
validate_error_message = "[prompt.invalid]Please enter a valid integer number"
|
||||
|
||||
|
||||
class FloatPrompt(PromptBase[int]):
|
||||
"""A prompt that returns a float."""
|
||||
"""A prompt that returns a float.
|
||||
|
||||
Example:
|
||||
>>> temperature = FloatPrompt.ask("Enter desired temperature")
|
||||
|
||||
"""
|
||||
|
||||
response_type = float
|
||||
validate_error_message = "[prompt.invalid]Please enter a number"
|
||||
|
||||
|
||||
class Confirm(PromptBase[bool]):
|
||||
"""A yes / no confirmation prompt."""
|
||||
"""A yes / no confirmation prompt.
|
||||
|
||||
Example:
|
||||
>>> if Confirm.ask("Continue"):
|
||||
run_job()
|
||||
|
||||
"""
|
||||
|
||||
response_type = bool
|
||||
prompt_suffix = "? "
|
||||
validate_error_message = "[prompt.invalid]Please enter Y or N"
|
||||
choices = ["y", "N"]
|
||||
|
||||
def process_response(self, value: str) -> PromptType:
|
||||
def process_response(self, value: str) -> bool:
|
||||
value = value.strip().lower()
|
||||
if value not in ["y", "n"]:
|
||||
raise InvalidResponse(self.validate_error_message)
|
||||
|
@ -287,17 +337,20 @@ if __name__ == "__main__": # pragma: no cover
|
|||
|
||||
from rich import print
|
||||
|
||||
if Confirm.ask("Run prompt tests"):
|
||||
if Confirm.ask("Run [i]prompt[/i] tests?"):
|
||||
while True:
|
||||
result = IntPrompt.ask("Enter a number betwen 1 and 10", default=5)
|
||||
result = IntPrompt.ask(
|
||||
":rocket: Enter a number betwen [b]1[/b] and [b]10[/b]", default=5
|
||||
)
|
||||
if result >= 1 and result <= 10:
|
||||
break
|
||||
print("[prompt.invalid]Number must be between 1 and 10")
|
||||
print(result)
|
||||
print(":pile_of_poo: [prompt.invalid]Number must be between 1 and 10")
|
||||
print(f"number={result}")
|
||||
|
||||
while True:
|
||||
password = Prompt.ask(
|
||||
"Please enter a password (must be at least 5 characters)", password=True
|
||||
"Please enter a password [cyan](must be at least 5 characters)",
|
||||
password=True,
|
||||
)
|
||||
if len(password) >= 5:
|
||||
break
|
||||
|
@ -308,5 +361,5 @@ if __name__ == "__main__": # pragma: no cover
|
|||
print(f"fruit={fruit!r}")
|
||||
|
||||
else:
|
||||
print("[b]OK")
|
||||
print("[b]OK :loudly_crying_face:")
|
||||
|
||||
|
|
70
tests/test_prompt.py
Normal file
70
tests/test_prompt.py
Normal file
|
@ -0,0 +1,70 @@
|
|||
import io
|
||||
|
||||
from rich.console import Console
|
||||
from rich.prompt import Prompt, IntPrompt, Confirm
|
||||
|
||||
|
||||
def test_prompt_str():
|
||||
INPUT = "egg\nfoo"
|
||||
console = Console(file=io.StringIO())
|
||||
name = Prompt.ask(
|
||||
"what is your name",
|
||||
console=console,
|
||||
choices=["foo", "bar"],
|
||||
default="baz",
|
||||
stream=io.StringIO(INPUT),
|
||||
)
|
||||
assert name == "foo"
|
||||
expected = "what is your name [foo/bar] (baz): Please select one of the available options\nwhat is your name [foo/bar] (baz): "
|
||||
output = console.file.getvalue()
|
||||
print(repr(output))
|
||||
assert output == expected
|
||||
|
||||
|
||||
def test_prompt_str_default():
|
||||
INPUT = ""
|
||||
console = Console(file=io.StringIO())
|
||||
name = Prompt.ask(
|
||||
"what is your name", console=console, default="Will", stream=io.StringIO(INPUT),
|
||||
)
|
||||
assert name == "Will"
|
||||
expected = "what is your name (Will): "
|
||||
output = console.file.getvalue()
|
||||
print(repr(output))
|
||||
assert output == expected
|
||||
|
||||
|
||||
def test_prompt_int():
|
||||
INPUT = "foo\n100"
|
||||
console = Console(file=io.StringIO())
|
||||
number = IntPrompt.ask(
|
||||
"Enter a number", console=console, stream=io.StringIO(INPUT),
|
||||
)
|
||||
assert number == 100
|
||||
expected = "Enter a number: Please enter a valid integer number\nEnter a number: "
|
||||
output = console.file.getvalue()
|
||||
print(repr(output))
|
||||
assert output == expected
|
||||
|
||||
|
||||
def test_prompt_confirm_no():
|
||||
INPUT = "foo\nNO\nn"
|
||||
console = Console(file=io.StringIO())
|
||||
answer = Confirm.ask("continue", console=console, stream=io.StringIO(INPUT),)
|
||||
assert answer is False
|
||||
expected = "continue [y/N]: Please enter Y or N\ncontinue [y/N]: Please enter Y or N\ncontinue [y/N]: "
|
||||
output = console.file.getvalue()
|
||||
print(repr(output))
|
||||
assert output == expected
|
||||
|
||||
|
||||
def test_prompt_confirm_yes():
|
||||
INPUT = "foo\nNO\ny"
|
||||
console = Console(file=io.StringIO())
|
||||
answer = Confirm.ask("continue", console=console, stream=io.StringIO(INPUT),)
|
||||
assert answer is True
|
||||
expected = "continue [y/N]: Please enter Y or N\ncontinue [y/N]: Please enter Y or N\ncontinue [y/N]: "
|
||||
output = console.file.getvalue()
|
||||
print(repr(output))
|
||||
assert output == expected
|
||||
|
Loading…
Add table
Add a link
Reference in a new issue