Mercurial > repos > shellac > guppy_basecaller
diff env/lib/python3.7/site-packages/humanfriendly/prompts.py @ 5:9b1c78e6ba9c draft default tip
"planemo upload commit 6c0a8142489327ece472c84e558c47da711a9142"
author | shellac |
---|---|
date | Mon, 01 Jun 2020 08:59:25 -0400 |
parents | 79f47841a781 |
children |
line wrap: on
line diff
--- a/env/lib/python3.7/site-packages/humanfriendly/prompts.py Thu May 14 16:47:39 2020 -0400 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,372 +0,0 @@ -# vim: fileencoding=utf-8 - -# Human friendly input/output in Python. -# -# Author: Peter Odding <peter@peterodding.com> -# Last Change: February 9, 2020 -# 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 - -# Public identifiers that require documentation. -__all__ = ( - 'MAX_ATTEMPTS', - 'TooManyInvalidReplies', - 'logger', - 'prepare_friendly_prompts', - 'prepare_prompt_text', - 'prompt_for_choice', - 'prompt_for_confirmation', - 'prompt_for_input', - 'retry_limit', -) - -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:`~humanfriendly.prompts.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:`~humanfriendly.prompts.retry_limit()`. - - Any exceptions raised by - :func:`~humanfriendly.prompts.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."""