This file is indexed.

/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."""