prompt update

This commit is contained in:
Will McGugan 2020-07-20 18:52:01 +01:00
parent a5a40b6300
commit 73de62b9fc
7 changed files with 172 additions and 30 deletions

View file

@ -7,3 +7,4 @@ exclude_lines =
pragma: no cover
if TYPE_CHECKING:
if __name__ == "__main__":
@overload

View file

@ -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

View file

@ -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

View file

@ -0,0 +1,5 @@
rich.progress
=============
.. automodule:: rich.prompt
:members:

View file

@ -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:

View file

@ -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
View 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