python (3.11.7)
       1  from typing import Any, Generic, List, Optional, TextIO, TypeVar, Union, overload
       2  
       3  from . import get_console
       4  from .console import Console
       5  from .text import Text, TextType
       6  
       7  PromptType = TypeVar("PromptType")
       8  DefaultType = TypeVar("DefaultType")
       9  
      10  
      11  class ESC[4;38;5;81mPromptError(ESC[4;38;5;149mException):
      12      """Exception base class for prompt related errors."""
      13  
      14  
      15  class ESC[4;38;5;81mInvalidResponse(ESC[4;38;5;149mPromptError):
      16      """Exception to indicate a response was invalid. Raise this within process_response() to indicate an error
      17      and provide an error message.
      18  
      19      Args:
      20          message (Union[str, Text]): Error message.
      21      """
      22  
      23      def __init__(self, message: TextType) -> None:
      24          self.message = message
      25  
      26      def __rich__(self) -> TextType:
      27          return self.message
      28  
      29  
      30  class ESC[4;38;5;81mPromptBase(ESC[4;38;5;149mGeneric[PromptType]):
      31      """Ask the user for input until a valid response is received. This is the base class, see one of
      32      the concrete classes for examples.
      33  
      34      Args:
      35          prompt (TextType, optional): Prompt text. Defaults to "".
      36          console (Console, optional): A Console instance or None to use global console. Defaults to None.
      37          password (bool, optional): Enable password input. Defaults to False.
      38          choices (List[str], optional): A list of valid choices. Defaults to None.
      39          show_default (bool, optional): Show default in prompt. Defaults to True.
      40          show_choices (bool, optional): Show choices in prompt. Defaults to True.
      41      """
      42  
      43      response_type: type = str
      44  
      45      validate_error_message = "[prompt.invalid]Please enter a valid value"
      46      illegal_choice_message = (
      47          "[prompt.invalid.choice]Please select one of the available options"
      48      )
      49      prompt_suffix = ": "
      50  
      51      choices: Optional[List[str]] = None
      52  
      53      def __init__(
      54          self,
      55          prompt: TextType = "",
      56          *,
      57          console: Optional[Console] = None,
      58          password: bool = False,
      59          choices: Optional[List[str]] = None,
      60          show_default: bool = True,
      61          show_choices: bool = True,
      62      ) -> None:
      63          self.console = console or get_console()
      64          self.prompt = (
      65              Text.from_markup(prompt, style="prompt")
      66              if isinstance(prompt, str)
      67              else prompt
      68          )
      69          self.password = password
      70          if choices is not None:
      71              self.choices = choices
      72          self.show_default = show_default
      73          self.show_choices = show_choices
      74  
      75      @classmethod
      76      @overload
      77      def ask(
      78          cls,
      79          prompt: TextType = "",
      80          *,
      81          console: Optional[Console] = None,
      82          password: bool = False,
      83          choices: Optional[List[str]] = None,
      84          show_default: bool = True,
      85          show_choices: bool = True,
      86          default: DefaultType,
      87          stream: Optional[TextIO] = None,
      88      ) -> Union[DefaultType, PromptType]:
      89          ...
      90  
      91      @classmethod
      92      @overload
      93      def ask(
      94          cls,
      95          prompt: TextType = "",
      96          *,
      97          console: Optional[Console] = None,
      98          password: bool = False,
      99          choices: Optional[List[str]] = None,
     100          show_default: bool = True,
     101          show_choices: bool = True,
     102          stream: Optional[TextIO] = None,
     103      ) -> PromptType:
     104          ...
     105  
     106      @classmethod
     107      def ask(
     108          cls,
     109          prompt: TextType = "",
     110          *,
     111          console: Optional[Console] = None,
     112          password: bool = False,
     113          choices: Optional[List[str]] = None,
     114          show_default: bool = True,
     115          show_choices: bool = True,
     116          default: Any = ...,
     117          stream: Optional[TextIO] = None,
     118      ) -> Any:
     119          """Shortcut to construct and run a prompt loop and return the result.
     120  
     121          Example:
     122              >>> filename = Prompt.ask("Enter a filename")
     123  
     124          Args:
     125              prompt (TextType, optional): Prompt text. Defaults to "".
     126              console (Console, optional): A Console instance or None to use global console. Defaults to None.
     127              password (bool, optional): Enable password input. Defaults to False.
     128              choices (List[str], optional): A list of valid choices. Defaults to None.
     129              show_default (bool, optional): Show default in prompt. Defaults to True.
     130              show_choices (bool, optional): Show choices in prompt. Defaults to True.
     131              stream (TextIO, optional): Optional text file open for reading to get input. Defaults to None.
     132          """
     133          _prompt = cls(
     134              prompt,
     135              console=console,
     136              password=password,
     137              choices=choices,
     138              show_default=show_default,
     139              show_choices=show_choices,
     140          )
     141          return _prompt(default=default, stream=stream)
     142  
     143      def render_default(self, default: DefaultType) -> Text:
     144          """Turn the supplied default in to a Text instance.
     145  
     146          Args:
     147              default (DefaultType): Default value.
     148  
     149          Returns:
     150              Text: Text containing rendering of default value.
     151          """
     152          return Text(f"({default})", "prompt.default")
     153  
     154      def make_prompt(self, default: DefaultType) -> Text:
     155          """Make prompt text.
     156  
     157          Args:
     158              default (DefaultType): Default value.
     159  
     160          Returns:
     161              Text: Text to display in prompt.
     162          """
     163          prompt = self.prompt.copy()
     164          prompt.end = ""
     165  
     166          if self.show_choices and self.choices:
     167              _choices = "/".join(self.choices)
     168              choices = f"[{_choices}]"
     169              prompt.append(" ")
     170              prompt.append(choices, "prompt.choices")
     171  
     172          if (
     173              default != ...
     174              and self.show_default
     175              and isinstance(default, (str, self.response_type))
     176          ):
     177              prompt.append(" ")
     178              _default = self.render_default(default)
     179              prompt.append(_default)
     180  
     181          prompt.append(self.prompt_suffix)
     182  
     183          return prompt
     184  
     185      @classmethod
     186      def get_input(
     187          cls,
     188          console: Console,
     189          prompt: TextType,
     190          password: bool,
     191          stream: Optional[TextIO] = None,
     192      ) -> str:
     193          """Get input from user.
     194  
     195          Args:
     196              console (Console): Console instance.
     197              prompt (TextType): Prompt text.
     198              password (bool): Enable password entry.
     199  
     200          Returns:
     201              str: String from user.
     202          """
     203          return console.input(prompt, password=password, stream=stream)
     204  
     205      def check_choice(self, value: str) -> bool:
     206          """Check value is in the list of valid choices.
     207  
     208          Args:
     209              value (str): Value entered by user.
     210  
     211          Returns:
     212              bool: True if choice was valid, otherwise False.
     213          """
     214          assert self.choices is not None
     215          return value.strip() in self.choices
     216  
     217      def process_response(self, value: str) -> PromptType:
     218          """Process response from user, convert to prompt type.
     219  
     220          Args:
     221              value (str): String typed by user.
     222  
     223          Raises:
     224              InvalidResponse: If ``value`` is invalid.
     225  
     226          Returns:
     227              PromptType: The value to be returned from ask method.
     228          """
     229          value = value.strip()
     230          try:
     231              return_value: PromptType = self.response_type(value)
     232          except ValueError:
     233              raise InvalidResponse(self.validate_error_message)
     234  
     235          if self.choices is not None and not self.check_choice(value):
     236              raise InvalidResponse(self.illegal_choice_message)
     237  
     238          return return_value
     239  
     240      def on_validate_error(self, value: str, error: InvalidResponse) -> None:
     241          """Called to handle validation error.
     242  
     243          Args:
     244              value (str): String entered by user.
     245              error (InvalidResponse): Exception instance the initiated the error.
     246          """
     247          self.console.print(error)
     248  
     249      def pre_prompt(self) -> None:
     250          """Hook to display something before the prompt."""
     251  
     252      @overload
     253      def __call__(self, *, stream: Optional[TextIO] = None) -> PromptType:
     254          ...
     255  
     256      @overload
     257      def __call__(
     258          self, *, default: DefaultType, stream: Optional[TextIO] = None
     259      ) -> Union[PromptType, DefaultType]:
     260          ...
     261  
     262      def __call__(self, *, default: Any = ..., stream: Optional[TextIO] = None) -> Any:
     263          """Run the prompt loop.
     264  
     265          Args:
     266              default (Any, optional): Optional default value.
     267  
     268          Returns:
     269              PromptType: Processed value.
     270          """
     271          while True:
     272              self.pre_prompt()
     273              prompt = self.make_prompt(default)
     274              value = self.get_input(self.console, prompt, self.password, stream=stream)
     275              if value == "" and default != ...:
     276                  return default
     277              try:
     278                  return_value = self.process_response(value)
     279              except InvalidResponse as error:
     280                  self.on_validate_error(value, error)
     281                  continue
     282              else:
     283                  return return_value
     284  
     285  
     286  class ESC[4;38;5;81mPrompt(ESC[4;38;5;149mPromptBase[str]):
     287      """A prompt that returns a str.
     288  
     289      Example:
     290          >>> name = Prompt.ask("Enter your name")
     291  
     292  
     293      """
     294  
     295      response_type = str
     296  
     297  
     298  class ESC[4;38;5;81mIntPrompt(ESC[4;38;5;149mPromptBase[int]):
     299      """A prompt that returns an integer.
     300  
     301      Example:
     302          >>> burrito_count = IntPrompt.ask("How many burritos do you want to order")
     303  
     304      """
     305  
     306      response_type = int
     307      validate_error_message = "[prompt.invalid]Please enter a valid integer number"
     308  
     309  
     310  class ESC[4;38;5;81mFloatPrompt(ESC[4;38;5;149mPromptBase[int]):
     311      """A prompt that returns a float.
     312  
     313      Example:
     314          >>> temperature = FloatPrompt.ask("Enter desired temperature")
     315  
     316      """
     317  
     318      response_type = float
     319      validate_error_message = "[prompt.invalid]Please enter a number"
     320  
     321  
     322  class ESC[4;38;5;81mConfirm(ESC[4;38;5;149mPromptBase[bool]):
     323      """A yes / no confirmation prompt.
     324  
     325      Example:
     326          >>> if Confirm.ask("Continue"):
     327                  run_job()
     328  
     329      """
     330  
     331      response_type = bool
     332      validate_error_message = "[prompt.invalid]Please enter Y or N"
     333      choices: List[str] = ["y", "n"]
     334  
     335      def render_default(self, default: DefaultType) -> Text:
     336          """Render the default as (y) or (n) rather than True/False."""
     337          yes, no = self.choices
     338          return Text(f"({yes})" if default else f"({no})", style="prompt.default")
     339  
     340      def process_response(self, value: str) -> bool:
     341          """Convert choices to a bool."""
     342          value = value.strip().lower()
     343          if value not in self.choices:
     344              raise InvalidResponse(self.validate_error_message)
     345          return value == self.choices[0]
     346  
     347  
     348  if __name__ == "__main__":  # pragma: no cover
     349  
     350      from pip._vendor.rich import print
     351  
     352      if Confirm.ask("Run [i]prompt[/i] tests?", default=True):
     353          while True:
     354              result = IntPrompt.ask(
     355                  ":rocket: Enter a number between [b]1[/b] and [b]10[/b]", default=5
     356              )
     357              if result >= 1 and result <= 10:
     358                  break
     359              print(":pile_of_poo: [prompt.invalid]Number must be between 1 and 10")
     360          print(f"number={result}")
     361  
     362          while True:
     363              password = Prompt.ask(
     364                  "Please enter a password [cyan](must be at least 5 characters)",
     365                  password=True,
     366              )
     367              if len(password) >= 5:
     368                  break
     369              print("[prompt.invalid]password too short")
     370          print(f"password={password!r}")
     371  
     372          fruit = Prompt.ask("Enter a fruit", choices=["apple", "orange", "pear"])
     373          print(f"fruit={fruit!r}")
     374  
     375      else:
     376          print("[b]OK :loudly_crying_face:")