图片解析应用
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

376 lines
16 KiB

  1. # vim: fileencoding=utf-8
  2. # Human friendly input/output in Python.
  3. #
  4. # Author: Peter Odding <peter@peterodding.com>
  5. # Last Change: February 9, 2020
  6. # URL: https://humanfriendly.readthedocs.io
  7. """
  8. Interactive terminal prompts.
  9. The :mod:`~humanfriendly.prompts` module enables interaction with the user
  10. (operator) by asking for confirmation (:func:`prompt_for_confirmation()`) and
  11. asking to choose from a list of options (:func:`prompt_for_choice()`). It works
  12. by rendering interactive prompts on the terminal.
  13. """
  14. # Standard library modules.
  15. import logging
  16. import sys
  17. # Modules included in our package.
  18. from humanfriendly.compat import interactive_prompt
  19. from humanfriendly.terminal import (
  20. HIGHLIGHT_COLOR,
  21. ansi_strip,
  22. ansi_wrap,
  23. connected_to_terminal,
  24. terminal_supports_colors,
  25. warning,
  26. )
  27. from humanfriendly.text import format, concatenate
  28. # Public identifiers that require documentation.
  29. __all__ = (
  30. 'MAX_ATTEMPTS',
  31. 'TooManyInvalidReplies',
  32. 'logger',
  33. 'prepare_friendly_prompts',
  34. 'prepare_prompt_text',
  35. 'prompt_for_choice',
  36. 'prompt_for_confirmation',
  37. 'prompt_for_input',
  38. 'retry_limit',
  39. )
  40. MAX_ATTEMPTS = 10
  41. """The number of times an interactive prompt is shown on invalid input (an integer)."""
  42. # Initialize a logger for this module.
  43. logger = logging.getLogger(__name__)
  44. def prompt_for_confirmation(question, default=None, padding=True):
  45. """
  46. Prompt the user for confirmation.
  47. :param question: The text that explains what the user is confirming (a string).
  48. :param default: The default value (a boolean) or :data:`None`.
  49. :param padding: Refer to the documentation of :func:`prompt_for_input()`.
  50. :returns: - If the user enters 'yes' or 'y' then :data:`True` is returned.
  51. - If the user enters 'no' or 'n' then :data:`False` is returned.
  52. - If the user doesn't enter any text or standard input is not
  53. connected to a terminal (which makes it impossible to prompt
  54. the user) the value of the keyword argument ``default`` is
  55. returned (if that value is not :data:`None`).
  56. :raises: - Any exceptions raised by :func:`retry_limit()`.
  57. - Any exceptions raised by :func:`prompt_for_input()`.
  58. When `default` is :data:`False` and the user doesn't enter any text an
  59. error message is printed and the prompt is repeated:
  60. >>> prompt_for_confirmation("Are you sure?")
  61. <BLANKLINE>
  62. Are you sure? [y/n]
  63. <BLANKLINE>
  64. Error: Please enter 'yes' or 'no' (there's no default choice).
  65. <BLANKLINE>
  66. Are you sure? [y/n]
  67. The same thing happens when the user enters text that isn't recognized:
  68. >>> prompt_for_confirmation("Are you sure?")
  69. <BLANKLINE>
  70. Are you sure? [y/n] about what?
  71. <BLANKLINE>
  72. Error: Please enter 'yes' or 'no' (the text 'about what?' is not recognized).
  73. <BLANKLINE>
  74. Are you sure? [y/n]
  75. """
  76. # Generate the text for the prompt.
  77. prompt_text = prepare_prompt_text(question, bold=True)
  78. # Append the valid replies (and default reply) to the prompt text.
  79. hint = "[Y/n]" if default else "[y/N]" if default is not None else "[y/n]"
  80. prompt_text += " %s " % prepare_prompt_text(hint, color=HIGHLIGHT_COLOR)
  81. # Loop until a valid response is given.
  82. logger.debug("Requesting interactive confirmation from terminal: %r", ansi_strip(prompt_text).rstrip())
  83. for attempt in retry_limit():
  84. reply = prompt_for_input(prompt_text, '', padding=padding, strip=True)
  85. if reply.lower() in ('y', 'yes'):
  86. logger.debug("Confirmation granted by reply (%r).", reply)
  87. return True
  88. elif reply.lower() in ('n', 'no'):
  89. logger.debug("Confirmation denied by reply (%r).", reply)
  90. return False
  91. elif (not reply) and default is not None:
  92. logger.debug("Default choice selected by empty reply (%r).",
  93. "granted" if default else "denied")
  94. return default
  95. else:
  96. details = ("the text '%s' is not recognized" % reply
  97. if reply else "there's no default choice")
  98. logger.debug("Got %s reply (%s), retrying (%i/%i) ..",
  99. "invalid" if reply else "empty", details,
  100. attempt, MAX_ATTEMPTS)
  101. warning("{indent}Error: Please enter 'yes' or 'no' ({details}).",
  102. indent=' ' if padding else '', details=details)
  103. def prompt_for_choice(choices, default=None, padding=True):
  104. """
  105. Prompt the user to select a choice from a group of options.
  106. :param choices: A sequence of strings with available options.
  107. :param default: The default choice if the user simply presses Enter
  108. (expected to be a string, defaults to :data:`None`).
  109. :param padding: Refer to the documentation of
  110. :func:`~humanfriendly.prompts.prompt_for_input()`.
  111. :returns: The string corresponding to the user's choice.
  112. :raises: - :exc:`~exceptions.ValueError` if `choices` is an empty sequence.
  113. - Any exceptions raised by
  114. :func:`~humanfriendly.prompts.retry_limit()`.
  115. - Any exceptions raised by
  116. :func:`~humanfriendly.prompts.prompt_for_input()`.
  117. When no options are given an exception is raised:
  118. >>> prompt_for_choice([])
  119. Traceback (most recent call last):
  120. File "humanfriendly/prompts.py", line 148, in prompt_for_choice
  121. raise ValueError("Can't prompt for choice without any options!")
  122. ValueError: Can't prompt for choice without any options!
  123. If a single option is given the user isn't prompted:
  124. >>> prompt_for_choice(['only one choice'])
  125. 'only one choice'
  126. Here's what the actual prompt looks like by default:
  127. >>> prompt_for_choice(['first option', 'second option'])
  128. <BLANKLINE>
  129. 1. first option
  130. 2. second option
  131. <BLANKLINE>
  132. Enter your choice as a number or unique substring (Control-C aborts): second
  133. <BLANKLINE>
  134. 'second option'
  135. If you don't like the whitespace (empty lines and indentation):
  136. >>> prompt_for_choice(['first option', 'second option'], padding=False)
  137. 1. first option
  138. 2. second option
  139. Enter your choice as a number or unique substring (Control-C aborts): first
  140. 'first option'
  141. """
  142. indent = ' ' if padding else ''
  143. # Make sure we can use 'choices' more than once (i.e. not a generator).
  144. choices = list(choices)
  145. if len(choices) == 1:
  146. # If there's only one option there's no point in prompting the user.
  147. logger.debug("Skipping interactive prompt because there's only option (%r).", choices[0])
  148. return choices[0]
  149. elif not choices:
  150. # We can't render a choice prompt without any options.
  151. raise ValueError("Can't prompt for choice without any options!")
  152. # Generate the prompt text.
  153. prompt_text = ('\n\n' if padding else '\n').join([
  154. # Present the available choices in a user friendly way.
  155. "\n".join([
  156. (u" %i. %s" % (i, choice)) + (" (default choice)" if choice == default else "")
  157. for i, choice in enumerate(choices, start=1)
  158. ]),
  159. # Instructions for the user.
  160. "Enter your choice as a number or unique substring (Control-C aborts): ",
  161. ])
  162. prompt_text = prepare_prompt_text(prompt_text, bold=True)
  163. # Loop until a valid choice is made.
  164. logger.debug("Requesting interactive choice on terminal (options are %s) ..",
  165. concatenate(map(repr, choices)))
  166. for attempt in retry_limit():
  167. reply = prompt_for_input(prompt_text, '', padding=padding, strip=True)
  168. if not reply and default is not None:
  169. logger.debug("Default choice selected by empty reply (%r).", default)
  170. return default
  171. elif reply.isdigit():
  172. index = int(reply) - 1
  173. if 0 <= index < len(choices):
  174. logger.debug("Option (%r) selected by numeric reply (%s).", choices[index], reply)
  175. return choices[index]
  176. # Check for substring matches.
  177. matches = []
  178. for choice in choices:
  179. lower_reply = reply.lower()
  180. lower_choice = choice.lower()
  181. if lower_reply == lower_choice:
  182. # If we have an 'exact' match we return it immediately.
  183. logger.debug("Option (%r) selected by reply (exact match).", choice)
  184. return choice
  185. elif lower_reply in lower_choice and len(lower_reply) > 0:
  186. # Otherwise we gather substring matches.
  187. matches.append(choice)
  188. if len(matches) == 1:
  189. # If a single choice was matched we return it.
  190. logger.debug("Option (%r) selected by reply (substring match on %r).", matches[0], reply)
  191. return matches[0]
  192. else:
  193. # Give the user a hint about what went wrong.
  194. if matches:
  195. details = format("text '%s' matches more than one choice: %s", reply, concatenate(matches))
  196. elif reply.isdigit():
  197. details = format("number %i is not a valid choice", int(reply))
  198. elif reply and not reply.isspace():
  199. details = format("text '%s' doesn't match any choices", reply)
  200. else:
  201. details = "there's no default choice"
  202. logger.debug("Got %s reply (%s), retrying (%i/%i) ..",
  203. "invalid" if reply else "empty", details,
  204. attempt, MAX_ATTEMPTS)
  205. warning("%sError: Invalid input (%s).", indent, details)
  206. def prompt_for_input(question, default=None, padding=True, strip=True):
  207. """
  208. Prompt the user for input (free form text).
  209. :param question: An explanation of what is expected from the user (a string).
  210. :param default: The return value if the user doesn't enter any text or
  211. standard input is not connected to a terminal (which
  212. makes it impossible to prompt the user).
  213. :param padding: Render empty lines before and after the prompt to make it
  214. stand out from the surrounding text? (a boolean, defaults
  215. to :data:`True`)
  216. :param strip: Strip leading/trailing whitespace from the user's reply?
  217. :returns: The text entered by the user (a string) or the value of the
  218. `default` argument.
  219. :raises: - :exc:`~exceptions.KeyboardInterrupt` when the program is
  220. interrupted_ while the prompt is active, for example
  221. because the user presses Control-C_.
  222. - :exc:`~exceptions.EOFError` when reading from `standard input`_
  223. fails, for example because the user presses Control-D_ or
  224. because the standard input stream is redirected (only if
  225. `default` is :data:`None`).
  226. .. _Control-C: https://en.wikipedia.org/wiki/Control-C#In_command-line_environments
  227. .. _Control-D: https://en.wikipedia.org/wiki/End-of-transmission_character#Meaning_in_Unix
  228. .. _interrupted: https://en.wikipedia.org/wiki/Unix_signal#SIGINT
  229. .. _standard input: https://en.wikipedia.org/wiki/Standard_streams#Standard_input_.28stdin.29
  230. """
  231. prepare_friendly_prompts()
  232. reply = None
  233. try:
  234. # Prefix an empty line to the text and indent by one space?
  235. if padding:
  236. question = '\n' + question
  237. question = question.replace('\n', '\n ')
  238. # Render the prompt and wait for the user's reply.
  239. try:
  240. reply = interactive_prompt(question)
  241. finally:
  242. if reply is None:
  243. # If the user terminated the prompt using Control-C or
  244. # Control-D instead of pressing Enter no newline will be
  245. # rendered after the prompt's text. The result looks kind of
  246. # weird:
  247. #
  248. # $ python -c 'print(raw_input("Are you sure? "))'
  249. # Are you sure? ^CTraceback (most recent call last):
  250. # File "<string>", line 1, in <module>
  251. # KeyboardInterrupt
  252. #
  253. # We can avoid this by emitting a newline ourselves if an
  254. # exception was raised (signaled by `reply' being None).
  255. sys.stderr.write('\n')
  256. if padding:
  257. # If the caller requested (didn't opt out of) `padding' then we'll
  258. # emit a newline regardless of whether an exception is being
  259. # handled. This helps to make interactive prompts `stand out' from
  260. # a surrounding `wall of text' on the terminal.
  261. sys.stderr.write('\n')
  262. except BaseException as e:
  263. if isinstance(e, EOFError) and default is not None:
  264. # If standard input isn't connected to an interactive terminal
  265. # but the caller provided a default we'll return that.
  266. logger.debug("Got EOF from terminal, returning default value (%r) ..", default)
  267. return default
  268. else:
  269. # Otherwise we log that the prompt was interrupted but propagate
  270. # the exception to the caller.
  271. logger.warning("Interactive prompt was interrupted by exception!", exc_info=True)
  272. raise
  273. if default is not None and not reply:
  274. # If the reply is empty and `default' is None we don't want to return
  275. # None because it's nicer for callers to be able to assume that the
  276. # return value is always a string.
  277. return default
  278. else:
  279. return reply.strip()
  280. def prepare_prompt_text(prompt_text, **options):
  281. """
  282. Wrap a text to be rendered as an interactive prompt in ANSI escape sequences.
  283. :param prompt_text: The text to render on the prompt (a string).
  284. :param options: Any keyword arguments are passed on to :func:`.ansi_wrap()`.
  285. :returns: The resulting prompt text (a string).
  286. ANSI escape sequences are only used when the standard output stream is
  287. connected to a terminal. When the standard input stream is connected to a
  288. terminal any escape sequences are wrapped in "readline hints".
  289. """
  290. return (ansi_wrap(prompt_text, readline_hints=connected_to_terminal(sys.stdin), **options)
  291. if terminal_supports_colors(sys.stdout)
  292. else prompt_text)
  293. def prepare_friendly_prompts():
  294. u"""
  295. Make interactive prompts more user friendly.
  296. The prompts presented by :func:`python2:raw_input()` (in Python 2) and
  297. :func:`python3:input()` (in Python 3) are not very user friendly by
  298. default, for example the cursor keys (:kbd:`←`, :kbd:`↑`, :kbd:`→` and
  299. :kbd:`↓`) and the :kbd:`Home` and :kbd:`End` keys enter characters instead
  300. of performing the action you would expect them to. By simply importing the
  301. :mod:`readline` module these prompts become much friendlier (as mentioned
  302. in the Python standard library documentation).
  303. This function is called by the other functions in this module to enable
  304. user friendly prompts.
  305. """
  306. try:
  307. import readline # NOQA
  308. except ImportError:
  309. # might not be available on Windows if pyreadline isn't installed
  310. pass
  311. def retry_limit(limit=MAX_ATTEMPTS):
  312. """
  313. Allow the user to provide valid input up to `limit` times.
  314. :param limit: The maximum number of attempts (a number,
  315. defaults to :data:`MAX_ATTEMPTS`).
  316. :returns: A generator of numbers starting from one.
  317. :raises: :exc:`TooManyInvalidReplies` when an interactive prompt
  318. receives repeated invalid input (:data:`MAX_ATTEMPTS`).
  319. This function returns a generator for interactive prompts that want to
  320. repeat on invalid input without getting stuck in infinite loops.
  321. """
  322. for i in range(limit):
  323. yield i + 1
  324. msg = "Received too many invalid replies on interactive prompt, giving up! (tried %i times)"
  325. formatted_msg = msg % limit
  326. # Make sure the event is logged.
  327. logger.warning(formatted_msg)
  328. # Force the caller to decide what to do now.
  329. raise TooManyInvalidReplies(formatted_msg)
  330. class TooManyInvalidReplies(Exception):
  331. """Raised by interactive prompts when they've received too many invalid inputs."""