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