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