meerschaum.utils.prompt

Functions for interacting with the user.

  1#! /usr/bin/env python
  2# -*- coding: utf-8 -*-
  3# vim:fenc=utf-8
  4
  5"""
  6Functions for interacting with the user.
  7"""
  8
  9from __future__ import annotations
 10import os
 11from meerschaum.utils.typing import Any, Union, Optional, Tuple, List
 12
 13
 14def prompt(
 15    question: str,
 16    icon: bool = True,
 17    default: Union[str, Tuple[str, str], None] = None,
 18    default_editable: Optional[str] = None,
 19    detect_password: bool = True,
 20    is_password: bool = False,
 21    wrap_lines: bool = True,
 22    noask: bool = False,
 23    **kw: Any
 24) -> str:
 25    """
 26    Ask the user a question and return the answer.
 27    Wrapper around `prompt_toolkit.prompt()` with modified behavior.
 28    For example, an empty string returns default instead of printing it for the user to delete
 29    (`prompt_toolkit` behavior).
 30
 31    Parameters
 32    ----------
 33    question: str
 34        The question to print to the user.
 35
 36    icon: bool, default True
 37        If True, prepend the configured icon.
 38
 39    default: Union[str, Tuple[str, str], None], default None
 40        If the response is '', return the default value.
 41
 42    default_editable: Optional[str], default None
 43        If provided, auto-type this user-editable string in the prompt.
 44
 45    detect_password: bool, default True
 46        If `True`, set the input method to a censored password box if the word `password`
 47        appears in the question.
 48
 49    is_password: default False
 50        If `True`, set the input method to a censored password box.
 51        May be overridden by `detect_password` unless `detect_password` is set to `False`.
 52
 53    wrap_lines: bool, default True
 54        If `True`, wrap the text across multiple lines.
 55        Flag is passed onto `prompt_toolkit`.
 56
 57    noask: bool, default False
 58        If `True`, only print the question and return the default answer.
 59
 60    Returns
 61    -------
 62    A `str` of the input provided by the user.
 63
 64    """
 65    from meerschaum.utils.packages import attempt_import
 66    from meerschaum.utils.formatting import colored, ANSI, CHARSET, highlight_pipes, fill_ansi
 67    from meerschaum.config import get_config
 68    from meerschaum.config.static import _static_config
 69    from meerschaum.utils.misc import filter_keywords
 70    from meerschaum.utils.daemon import running_in_daemon
 71    noask = check_noask(noask)
 72    if not noask:
 73        prompt_toolkit = attempt_import('prompt_toolkit')
 74    question_config = get_config('formatting', 'question', patch=True)
 75
 76    ### if a default is provided, append it to the question.
 77    default_answer = default
 78    if default is not None:
 79        question += " (default: "
 80        if isinstance(default, tuple) and len(default) > 1:
 81            question += f"{default[0]} [{default[1]}]"
 82            default_answer = default[0]
 83        else:
 84            question += f"{default}"
 85        question += ")"
 86
 87    ### detect password
 88    if (detect_password and 'password' in question.lower()) or is_password:
 89        kw['is_password'] = True
 90
 91    ### Add the icon and only color the first line.
 92    lines = question.split('\n')
 93    first_line = lines[0]
 94    other_lines = '' if len(lines) <= 1 else '\n'.join(lines[1:])
 95
 96    if ANSI:
 97        first_line = fill_ansi(highlight_pipes(first_line), **question_config['ansi']['rich'])
 98        other_lines = highlight_pipes(other_lines)
 99
100    _icon = question_config[CHARSET]['icon']
101    question = (' ' + _icon + ' ') if icon and len(_icon) > 0 else ''
102    question += first_line
103    if len(other_lines) > 0:
104        question += '\n' + other_lines
105    question += ' '
106
107    if not running_in_daemon():
108        answer = (
109            prompt_toolkit.prompt(
110                prompt_toolkit.formatted_text.ANSI(question),
111                wrap_lines=wrap_lines,
112                default=default_editable or '',
113                **filter_keywords(prompt_toolkit.prompt, **kw)
114            ) if not noask else ''
115        )
116    else:
117        print(question, end='\n', flush=True)
118        try:
119            answer = input() if not noask else ''
120        except EOFError:
121            answer = ''
122    if noask:
123        print(question)
124    if answer == '' and default is not None:
125        return default_answer
126    return answer
127
128
129def yes_no(
130        question: str = '',
131        options: Tuple[str, str] = ('y', 'n'),
132        default: str = 'y',
133        wrappers: Tuple[str, str] = ('[', ']'),
134        icon: bool = True,
135        yes: bool = False,
136        noask: bool = False,
137        **kw : Any
138    ) -> bool:
139    """
140    Print a question and prompt the user with a yes / no input.
141    Returns `True` for `'yes'`, False for `'no'`.
142
143    Parameters
144    ----------
145    question: str, default ''
146        The question to print to the user.
147
148    options: Tuple[str, str], default ('y', 'n')
149        The `y/n` options. The first is considered `True`, and all options must be lower case.
150
151    default: str, default y
152        The default option. Is represented with a capital to distinguish that it's the default.
153
154    wrappers: Tuple[str, str], default ('[', ']')
155        Text to print around the '[y/n]' options.
156
157    icon: bool, default True
158        If True, prepend the configured question icon.
159
160    Returns
161    -------
162    A bool indicating the user's choice.
163
164    Examples
165    --------
166    ```python-repl
167    >>> yes_no("Do you like me?", default='y')
168     ❓ Do you like me? [Y/n]
169    True
170    >>> yes_no("Cats or dogs?", options=('cats', 'dogs'))
171     ❓ Cats or dogs? [cats/dogs]
172     Please enter a valid response.
173     ❓ Cats or dogs? [cats/dogs] dogs
174    False
175    ```
176    """
177    from meerschaum.utils.warnings import error, warn
178    from meerschaum.utils.formatting import ANSI, UNICODE
179    from meerschaum.utils.packages import attempt_import
180
181    default = options[0] if yes else default
182    noask = yes or check_noask(noask)
183
184    ending = f" {wrappers[0]}" + "/".join(
185        [
186            o.upper() if o.lower() == default.lower()
187            else o.lower() for o in options
188        ]
189    ) + f"{wrappers[1]}"
190    while True:
191        try:
192            answer = prompt(question + ending, icon=icon, detect_password=False, noask=noask)
193            success = True
194        except KeyboardInterrupt:
195            success = False
196        
197        if not success:
198            error(f"Error getting response. Aborting...", stack=False)
199        if answer == "":
200            answer = default
201
202        if answer.lower() in options:
203            break
204        warn('Please enter a valid reponse.', stack=False)
205    
206    return answer.lower() == options[0].lower()
207
208def choose(
209        question: str,
210        choices: List[Union[str, Tuple[str, str]]],
211        default: Union[str, List[str], None] = None,
212        numeric: bool = True,
213        multiple: bool = False,
214        as_indices: bool = False,
215        delimiter: str = ',',
216        icon: bool = True,
217        warn: bool = True,
218        noask: bool = False,
219        **kw
220    ) -> Union[str, Tuple[str], None]:
221    """
222    Present a list of options and return the user's choice.
223
224    Parameters
225    ----------
226    question: str
227        The question to be printed.
228
229    choices: List[Union[str, Tuple[str, str]]
230        A list of options.
231        If an option is a tuple of two strings, the first string is treated as the index
232        and not displayed. In this case, set `as_indices` to `True` to return the index.
233
234    default: Union[str, List[str], None], default None
235        If the user declines to enter a choice, return this value.
236
237    numeric: bool, default True
238        If `True`, number the items in the list and ask for a number as input.
239        If `False`, require the user to type the complete string.
240
241    multiple: bool, default False
242        If `True`, allow the user to choose multiple answers separated by `delimiter`.
243
244    as_indices: bool, default False
245        If `True`, return the indices for the choices.
246        If a choice is a tuple of two strings, the first is assumed to be the index.
247        Otherwise the index in the list is returned.
248
249    delimiter: str, default ','
250        If `multiple`, separate answers by this string. Raise a warning if this string is contained
251        in any of the choices.
252
253    icon: bool, default True
254        If `True`, include the question icon.
255
256    warn: bool, default True
257        If `True`, raise warnings when invalid input is entered.
258
259    noask: bool, default False
260        If `True`, skip printing the question and return the default value.
261
262    Returns
263    -------
264    A string for a single answer or a tuple of strings if `multiple` is `True`.
265
266    """
267    from meerschaum.utils.warnings import warn as _warn
268    from meerschaum.utils.packages import attempt_import
269    from meerschaum.utils.misc import print_options
270    noask = check_noask(noask)
271
272    ### Handle empty choices.
273    if len(choices) == 0:
274        _warn(f"No available choices. Returning default value '{default}'.", stacklevel=3)
275        return default
276
277    ### If the default case is to include multiple answers, allow for multiple inputs.
278    if isinstance(default, list):
279        multiple = True
280
281    choices_indices = {}
282    for i, c in enumerate(choices):
283        if isinstance(c, tuple):
284            i, c = c
285        choices_indices[i] = c
286
287    def _enforce_default(d):
288        if d is not None and d not in choices and d not in choices_indices and warn:
289            _warn(
290                f"Default choice '{default}' is not contained in the choices {choices}. "
291                + "Setting numeric = False.",
292                stacklevel = 3
293            )
294            return False
295        return True
296
297    ### Throw a warning if the default isn't a choice.
298    for d in (default if isinstance(default, list) else [default]):
299        if not _enforce_default(d):
300            numeric = False
301            break
302
303    _default = default
304    _choices = list(choices_indices.values())
305    if multiple:
306        question += f"\n    Enter your choices, separated by '{delimiter}'.\n"
307
308    altered_choices = {}
309    altered_indices = {}
310    altered_default_indices = {}
311    delim_replacement = '_' if delimiter != '_' else '-'
312    can_strip_start_spaces, can_strip_end_spaces = True, True
313    for i, c in choices_indices.items():
314        if isinstance(c, tuple):
315            key, c = c
316        if can_strip_start_spaces and c.startswith(' '):
317            can_strip_start_spaces = False
318        if can_strip_end_spaces and c.endswith(' '):
319            can_strip_end_spaces = False
320
321    if multiple:
322        ### Check if the defaults have the delimiter.
323        for i, d in enumerate(default if isinstance(default, list) else [default]):
324            if d is None or delimiter not in d:
325                continue
326            new_d = d.replace(delimiter, delim_replacement)
327            altered_choices[new_d] = d
328            altered_default_indices[i] = new_d
329        for i, new_d in altered_default_indices.items():
330            if not isinstance(default, list):
331                default = new_d
332                break
333            default[i] = new_d
334
335        ### Check if the choices have the delimiter.
336        for i, c in choices_indices.items():
337            if delimiter in c and not numeric and warn:
338                _warn(
339                    f"The delimiter '{delimiter}' is contained within choice '{c}'.\n"
340                    + f"Replacing the string '{delimiter}' with '{delim_replacement}' in "
341                    + "the choice for correctly parsing input (will be replaced upon returning the prompt).",
342                    stacklevel = 3,
343                )
344                new_c = c.replace(delimiter, delim_replacement)
345                altered_choices[new_c] = c
346                altered_indices[i] = new_c
347        for i, new_c in altered_indices.items():
348            choices_indices[i] = new_c
349        default = delimiter.join(default) if isinstance(default, list) else default
350
351    question_options = []
352    if numeric:
353        _choices = [str(i + 1) for i, c in enumerate(choices)]
354        _default = ''
355        if default is not None:
356            for d in (default.split(delimiter) if multiple else [default]):
357                if d not in choices and d in choices_indices:
358                    d_index = d
359                    d_value = choices_indices[d]
360                    for _i, _option in enumerate(choices):
361                        if (
362                            isinstance(_option, tuple) and (
363                                _option[1] == d_value
364                                or
365                                _option[0] == d_index
366                            )
367                        ) or d_index == _i:
368                            d = _option
369
370                _d = str(choices.index(d) + 1)
371                _default += _d + delimiter
372        _default = _default[:-1 * len(delimiter)]
373        #  question += '\n'
374        choices_digits = len(str(len(choices)))
375        for i, c in enumerate(choices_indices.values()):
376            question_options.append(
377                f"  {i + 1}. "
378                + (" " * (choices_digits - len(str(i + 1))))
379                + f"{c}\n"
380            )
381        default_tuple = (_default, default) if default is not None else None
382    else:
383        default_tuple = default
384        #  question += '\n'
385        for c in choices_indices.values():
386            question_options.append(f"{c}\n")
387
388    if 'completer' not in kw:
389        WordCompleter = attempt_import('prompt_toolkit.completion').WordCompleter
390        kw['completer'] = WordCompleter(choices_indices.values(), sentence=True)
391
392    valid = False
393    while not valid:
394        print_options(question_options, header='')
395        answer = prompt(
396            question,
397            icon = icon,
398            default = default_tuple,
399            noask = noask,
400            **kw
401        )
402        ### Split along the delimiter.
403        _answers = [answer] if not multiple else [a for a in answer.split(delimiter)]
404
405        ### Remove trailing spaces if possible.
406        _answers = [(_a.rstrip(' ') if can_strip_end_spaces else _a) for _a in _answers]
407
408        ### Remove leading spaces if possible.
409        _answers = [(_a.lstrip(' ') if can_strip_start_spaces else _a) for _a in _answers]
410
411        ### Remove empty strings.
412        _answers = [_a for _a in _answers if _a]
413
414        if multiple and len(_answers) == 0:
415            _answers = default_tuple if isinstance(default_tuple, list) else [default_tuple]
416        answers = [altered_choices.get(a, a) for a in _answers]
417
418        valid = (len(answers) > 1 or not (len(answers) == 1 and answers[0] is None))
419        for a in answers:
420            if (
421                not a in {_original for _new, _original in altered_choices.items()}
422                and not a in _choices
423                and a != default
424                and not noask
425            ):
426                valid = False
427                break
428        if valid:
429            break
430        if warn:
431            _warn(f"Please pick a valid choice.", stack=False)
432
433    if not multiple:
434        if not numeric:
435            return answer
436        try:
437            _answer = choices[int(answer) - 1]
438            if as_indices and isinstance(choice, tuple):
439                return _answer[0]
440            return _answer
441        except Exception as e:
442            _warn(f"Could not cast answer '{answer}' to an integer.", stacklevel=3)
443
444    if not numeric:
445        return answers
446    _answers = []
447    for a in answers:
448        try:
449            _answer = choices[int(a) - 1]
450            _answer_to_return = altered_choices.get(_answer, _answer)
451            if isinstance(_answer_to_return, tuple) and as_indices:
452                _answer_to_return = _answer_to_return[0]
453            _answers.append(_answer_to_return)
454        except Exception as e:
455            _warn(f"Could not cast answer '{a}' to an integer.", stacklevel=3)
456    return _answers
457
458
459def get_password(
460        username: Optional[str] = None,
461        minimum_length: Optional[int] = None,
462        confirm: bool = True,
463        **kw: Any
464    ) -> str:
465    """
466    Prompt the user for a password.
467
468    Parameters
469    ----------
470    username: Optional[str], default None
471        If provided, print the username when asking for a password.
472
473    minimum_length: Optional[int], default None
474        If provided, enforce a password of at least this length.
475
476    confirm: bool, default True
477        If `True`, prompt the user for a password twice.
478
479    Returns
480    -------
481    The password string (censored from terminal output when typing).
482
483    Examples
484    --------
485    ```python-repl
486    >>> get_password()
487     ❓ Password: *******
488     ❓ Confirm password: *******
489    'hunter2'
490    ```
491
492    """
493    from meerschaum.utils.warnings import warn
494    while True:
495        password = prompt(
496            f"Password" + (f" for user '{username}':" if username is not None else ":"),
497            is_password = True,
498            **kw
499        )
500        if minimum_length is not None and len(password) < minimum_length:
501            warn(
502                "Password is too short. " +
503                f"Please enter a password that is at least {minimum_length} characters.",
504                stack = False
505            )
506            continue
507
508        if not confirm:
509            return password
510
511        _password = prompt(
512            f"Confirm password" + (f" for user '{username}':" if username is not None else ":"),
513            is_password = True,
514            **kw
515        )
516        if password != _password:
517            warn(f"Passwords do not match! Please try again.", stack=False)
518            continue
519        else:
520            return password
521
522
523def get_email(username: Optional[str] = None, allow_omit: bool = True, **kw: Any) -> str:
524    """
525    Prompt the user for an email and enforce that it's valid.
526
527    Parameters
528    ----------
529    username: Optional[str], default None
530        If provided, print the username in the prompt.
531
532    allow_omit: bool, default True
533        If `True`, allow the user to omit the email.
534
535    Returns
536    -------
537    The provided email string.
538
539    Examples
540    --------
541    ```python-repl
542    >>> get_email()
543     ❓ Email (empty to omit): foo@foo
544     Invalid email! Please try again.
545     ❓ Email (empty to omit): foo@foo.com
546    'foo@foo.com'
547    ```
548    """
549    from meerschaum.utils.warnings import warn
550    from meerschaum.utils.misc import is_valid_email
551    while True:
552        email = prompt(
553            f"Email" + (f" for user '{username}'" if username is not None else "") +
554            (" (empty to omit):" if allow_omit else ": "),
555            **kw
556        )
557        if (allow_omit and email == '') or is_valid_email(email):
558            return email
559        warn(f"Invalid email! Please try again.", stack=False)
560
561
562def check_noask(noask: bool = False) -> bool:
563    """
564    Flip `noask` to `True` if `MRSM_NOASK` is set.
565    """
566    from meerschaum.config.static import STATIC_CONFIG
567    NOASK = STATIC_CONFIG['environment']['noask']
568    if noask:
569        return True
570    return (
571        os.environ.get(NOASK, 'false').lower()
572        in ('1', 'true')
573    )
def prompt( question: str, icon: bool = True, default: Union[str, Tuple[str, str], NoneType] = None, default_editable: Optional[str] = None, detect_password: bool = True, is_password: bool = False, wrap_lines: bool = True, noask: bool = False, **kw: Any) -> str:
 15def prompt(
 16    question: str,
 17    icon: bool = True,
 18    default: Union[str, Tuple[str, str], None] = None,
 19    default_editable: Optional[str] = None,
 20    detect_password: bool = True,
 21    is_password: bool = False,
 22    wrap_lines: bool = True,
 23    noask: bool = False,
 24    **kw: Any
 25) -> str:
 26    """
 27    Ask the user a question and return the answer.
 28    Wrapper around `prompt_toolkit.prompt()` with modified behavior.
 29    For example, an empty string returns default instead of printing it for the user to delete
 30    (`prompt_toolkit` behavior).
 31
 32    Parameters
 33    ----------
 34    question: str
 35        The question to print to the user.
 36
 37    icon: bool, default True
 38        If True, prepend the configured icon.
 39
 40    default: Union[str, Tuple[str, str], None], default None
 41        If the response is '', return the default value.
 42
 43    default_editable: Optional[str], default None
 44        If provided, auto-type this user-editable string in the prompt.
 45
 46    detect_password: bool, default True
 47        If `True`, set the input method to a censored password box if the word `password`
 48        appears in the question.
 49
 50    is_password: default False
 51        If `True`, set the input method to a censored password box.
 52        May be overridden by `detect_password` unless `detect_password` is set to `False`.
 53
 54    wrap_lines: bool, default True
 55        If `True`, wrap the text across multiple lines.
 56        Flag is passed onto `prompt_toolkit`.
 57
 58    noask: bool, default False
 59        If `True`, only print the question and return the default answer.
 60
 61    Returns
 62    -------
 63    A `str` of the input provided by the user.
 64
 65    """
 66    from meerschaum.utils.packages import attempt_import
 67    from meerschaum.utils.formatting import colored, ANSI, CHARSET, highlight_pipes, fill_ansi
 68    from meerschaum.config import get_config
 69    from meerschaum.config.static import _static_config
 70    from meerschaum.utils.misc import filter_keywords
 71    from meerschaum.utils.daemon import running_in_daemon
 72    noask = check_noask(noask)
 73    if not noask:
 74        prompt_toolkit = attempt_import('prompt_toolkit')
 75    question_config = get_config('formatting', 'question', patch=True)
 76
 77    ### if a default is provided, append it to the question.
 78    default_answer = default
 79    if default is not None:
 80        question += " (default: "
 81        if isinstance(default, tuple) and len(default) > 1:
 82            question += f"{default[0]} [{default[1]}]"
 83            default_answer = default[0]
 84        else:
 85            question += f"{default}"
 86        question += ")"
 87
 88    ### detect password
 89    if (detect_password and 'password' in question.lower()) or is_password:
 90        kw['is_password'] = True
 91
 92    ### Add the icon and only color the first line.
 93    lines = question.split('\n')
 94    first_line = lines[0]
 95    other_lines = '' if len(lines) <= 1 else '\n'.join(lines[1:])
 96
 97    if ANSI:
 98        first_line = fill_ansi(highlight_pipes(first_line), **question_config['ansi']['rich'])
 99        other_lines = highlight_pipes(other_lines)
100
101    _icon = question_config[CHARSET]['icon']
102    question = (' ' + _icon + ' ') if icon and len(_icon) > 0 else ''
103    question += first_line
104    if len(other_lines) > 0:
105        question += '\n' + other_lines
106    question += ' '
107
108    if not running_in_daemon():
109        answer = (
110            prompt_toolkit.prompt(
111                prompt_toolkit.formatted_text.ANSI(question),
112                wrap_lines=wrap_lines,
113                default=default_editable or '',
114                **filter_keywords(prompt_toolkit.prompt, **kw)
115            ) if not noask else ''
116        )
117    else:
118        print(question, end='\n', flush=True)
119        try:
120            answer = input() if not noask else ''
121        except EOFError:
122            answer = ''
123    if noask:
124        print(question)
125    if answer == '' and default is not None:
126        return default_answer
127    return answer

Ask the user a question and return the answer. Wrapper around prompt_toolkit.prompt() with modified behavior. For example, an empty string returns default instead of printing it for the user to delete (prompt_toolkit behavior).

Parameters
  • question (str): The question to print to the user.
  • icon (bool, default True): If True, prepend the configured icon.
  • default (Union[str, Tuple[str, str], None], default None): If the response is '', return the default value.
  • default_editable (Optional[str], default None): If provided, auto-type this user-editable string in the prompt.
  • detect_password (bool, default True): If True, set the input method to a censored password box if the word password appears in the question.
  • is_password (default False): If True, set the input method to a censored password box. May be overridden by detect_password unless detect_password is set to False.
  • wrap_lines (bool, default True): If True, wrap the text across multiple lines. Flag is passed onto prompt_toolkit.
  • noask (bool, default False): If True, only print the question and return the default answer.
Returns
  • A str of the input provided by the user.
def yes_no( question: str = '', options: Tuple[str, str] = ('y', 'n'), default: str = 'y', wrappers: Tuple[str, str] = ('[', ']'), icon: bool = True, yes: bool = False, noask: bool = False, **kw: Any) -> bool:
130def yes_no(
131        question: str = '',
132        options: Tuple[str, str] = ('y', 'n'),
133        default: str = 'y',
134        wrappers: Tuple[str, str] = ('[', ']'),
135        icon: bool = True,
136        yes: bool = False,
137        noask: bool = False,
138        **kw : Any
139    ) -> bool:
140    """
141    Print a question and prompt the user with a yes / no input.
142    Returns `True` for `'yes'`, False for `'no'`.
143
144    Parameters
145    ----------
146    question: str, default ''
147        The question to print to the user.
148
149    options: Tuple[str, str], default ('y', 'n')
150        The `y/n` options. The first is considered `True`, and all options must be lower case.
151
152    default: str, default y
153        The default option. Is represented with a capital to distinguish that it's the default.
154
155    wrappers: Tuple[str, str], default ('[', ']')
156        Text to print around the '[y/n]' options.
157
158    icon: bool, default True
159        If True, prepend the configured question icon.
160
161    Returns
162    -------
163    A bool indicating the user's choice.
164
165    Examples
166    --------
167    ```python-repl
168    >>> yes_no("Do you like me?", default='y')
169     ❓ Do you like me? [Y/n]
170    True
171    >>> yes_no("Cats or dogs?", options=('cats', 'dogs'))
172     ❓ Cats or dogs? [cats/dogs]
173     Please enter a valid response.
174     ❓ Cats or dogs? [cats/dogs] dogs
175    False
176    ```
177    """
178    from meerschaum.utils.warnings import error, warn
179    from meerschaum.utils.formatting import ANSI, UNICODE
180    from meerschaum.utils.packages import attempt_import
181
182    default = options[0] if yes else default
183    noask = yes or check_noask(noask)
184
185    ending = f" {wrappers[0]}" + "/".join(
186        [
187            o.upper() if o.lower() == default.lower()
188            else o.lower() for o in options
189        ]
190    ) + f"{wrappers[1]}"
191    while True:
192        try:
193            answer = prompt(question + ending, icon=icon, detect_password=False, noask=noask)
194            success = True
195        except KeyboardInterrupt:
196            success = False
197        
198        if not success:
199            error(f"Error getting response. Aborting...", stack=False)
200        if answer == "":
201            answer = default
202
203        if answer.lower() in options:
204            break
205        warn('Please enter a valid reponse.', stack=False)
206    
207    return answer.lower() == options[0].lower()

Print a question and prompt the user with a yes / no input. Returns True for 'yes', False for 'no'.

Parameters
  • question (str, default ''): The question to print to the user.
  • options (Tuple[str, str], default ('y', 'n')): The y/n options. The first is considered True, and all options must be lower case.
  • default (str, default y): The default option. Is represented with a capital to distinguish that it's the default.
  • wrappers (Tuple[str, str], default ('[', ']')): Text to print around the '[y/n]' options.
  • icon (bool, default True): If True, prepend the configured question icon.
Returns
  • A bool indicating the user's choice.
Examples
>>> yes_no("Do you like me?", default='y')
 ❓ Do you like me? [Y/n]
True
>>> yes_no("Cats or dogs?", options=('cats', 'dogs'))
 ❓ Cats or dogs? [cats/dogs]
 Please enter a valid response.
 ❓ Cats or dogs? [cats/dogs] dogs
False
def choose( question: str, choices: List[Union[str, Tuple[str, str]]], default: Union[str, List[str], NoneType] = None, numeric: bool = True, multiple: bool = False, as_indices: bool = False, delimiter: str = ',', icon: bool = True, warn: bool = True, noask: bool = False, **kw) -> Union[str, Tuple[str], NoneType]:
209def choose(
210        question: str,
211        choices: List[Union[str, Tuple[str, str]]],
212        default: Union[str, List[str], None] = None,
213        numeric: bool = True,
214        multiple: bool = False,
215        as_indices: bool = False,
216        delimiter: str = ',',
217        icon: bool = True,
218        warn: bool = True,
219        noask: bool = False,
220        **kw
221    ) -> Union[str, Tuple[str], None]:
222    """
223    Present a list of options and return the user's choice.
224
225    Parameters
226    ----------
227    question: str
228        The question to be printed.
229
230    choices: List[Union[str, Tuple[str, str]]
231        A list of options.
232        If an option is a tuple of two strings, the first string is treated as the index
233        and not displayed. In this case, set `as_indices` to `True` to return the index.
234
235    default: Union[str, List[str], None], default None
236        If the user declines to enter a choice, return this value.
237
238    numeric: bool, default True
239        If `True`, number the items in the list and ask for a number as input.
240        If `False`, require the user to type the complete string.
241
242    multiple: bool, default False
243        If `True`, allow the user to choose multiple answers separated by `delimiter`.
244
245    as_indices: bool, default False
246        If `True`, return the indices for the choices.
247        If a choice is a tuple of two strings, the first is assumed to be the index.
248        Otherwise the index in the list is returned.
249
250    delimiter: str, default ','
251        If `multiple`, separate answers by this string. Raise a warning if this string is contained
252        in any of the choices.
253
254    icon: bool, default True
255        If `True`, include the question icon.
256
257    warn: bool, default True
258        If `True`, raise warnings when invalid input is entered.
259
260    noask: bool, default False
261        If `True`, skip printing the question and return the default value.
262
263    Returns
264    -------
265    A string for a single answer or a tuple of strings if `multiple` is `True`.
266
267    """
268    from meerschaum.utils.warnings import warn as _warn
269    from meerschaum.utils.packages import attempt_import
270    from meerschaum.utils.misc import print_options
271    noask = check_noask(noask)
272
273    ### Handle empty choices.
274    if len(choices) == 0:
275        _warn(f"No available choices. Returning default value '{default}'.", stacklevel=3)
276        return default
277
278    ### If the default case is to include multiple answers, allow for multiple inputs.
279    if isinstance(default, list):
280        multiple = True
281
282    choices_indices = {}
283    for i, c in enumerate(choices):
284        if isinstance(c, tuple):
285            i, c = c
286        choices_indices[i] = c
287
288    def _enforce_default(d):
289        if d is not None and d not in choices and d not in choices_indices and warn:
290            _warn(
291                f"Default choice '{default}' is not contained in the choices {choices}. "
292                + "Setting numeric = False.",
293                stacklevel = 3
294            )
295            return False
296        return True
297
298    ### Throw a warning if the default isn't a choice.
299    for d in (default if isinstance(default, list) else [default]):
300        if not _enforce_default(d):
301            numeric = False
302            break
303
304    _default = default
305    _choices = list(choices_indices.values())
306    if multiple:
307        question += f"\n    Enter your choices, separated by '{delimiter}'.\n"
308
309    altered_choices = {}
310    altered_indices = {}
311    altered_default_indices = {}
312    delim_replacement = '_' if delimiter != '_' else '-'
313    can_strip_start_spaces, can_strip_end_spaces = True, True
314    for i, c in choices_indices.items():
315        if isinstance(c, tuple):
316            key, c = c
317        if can_strip_start_spaces and c.startswith(' '):
318            can_strip_start_spaces = False
319        if can_strip_end_spaces and c.endswith(' '):
320            can_strip_end_spaces = False
321
322    if multiple:
323        ### Check if the defaults have the delimiter.
324        for i, d in enumerate(default if isinstance(default, list) else [default]):
325            if d is None or delimiter not in d:
326                continue
327            new_d = d.replace(delimiter, delim_replacement)
328            altered_choices[new_d] = d
329            altered_default_indices[i] = new_d
330        for i, new_d in altered_default_indices.items():
331            if not isinstance(default, list):
332                default = new_d
333                break
334            default[i] = new_d
335
336        ### Check if the choices have the delimiter.
337        for i, c in choices_indices.items():
338            if delimiter in c and not numeric and warn:
339                _warn(
340                    f"The delimiter '{delimiter}' is contained within choice '{c}'.\n"
341                    + f"Replacing the string '{delimiter}' with '{delim_replacement}' in "
342                    + "the choice for correctly parsing input (will be replaced upon returning the prompt).",
343                    stacklevel = 3,
344                )
345                new_c = c.replace(delimiter, delim_replacement)
346                altered_choices[new_c] = c
347                altered_indices[i] = new_c
348        for i, new_c in altered_indices.items():
349            choices_indices[i] = new_c
350        default = delimiter.join(default) if isinstance(default, list) else default
351
352    question_options = []
353    if numeric:
354        _choices = [str(i + 1) for i, c in enumerate(choices)]
355        _default = ''
356        if default is not None:
357            for d in (default.split(delimiter) if multiple else [default]):
358                if d not in choices and d in choices_indices:
359                    d_index = d
360                    d_value = choices_indices[d]
361                    for _i, _option in enumerate(choices):
362                        if (
363                            isinstance(_option, tuple) and (
364                                _option[1] == d_value
365                                or
366                                _option[0] == d_index
367                            )
368                        ) or d_index == _i:
369                            d = _option
370
371                _d = str(choices.index(d) + 1)
372                _default += _d + delimiter
373        _default = _default[:-1 * len(delimiter)]
374        #  question += '\n'
375        choices_digits = len(str(len(choices)))
376        for i, c in enumerate(choices_indices.values()):
377            question_options.append(
378                f"  {i + 1}. "
379                + (" " * (choices_digits - len(str(i + 1))))
380                + f"{c}\n"
381            )
382        default_tuple = (_default, default) if default is not None else None
383    else:
384        default_tuple = default
385        #  question += '\n'
386        for c in choices_indices.values():
387            question_options.append(f"{c}\n")
388
389    if 'completer' not in kw:
390        WordCompleter = attempt_import('prompt_toolkit.completion').WordCompleter
391        kw['completer'] = WordCompleter(choices_indices.values(), sentence=True)
392
393    valid = False
394    while not valid:
395        print_options(question_options, header='')
396        answer = prompt(
397            question,
398            icon = icon,
399            default = default_tuple,
400            noask = noask,
401            **kw
402        )
403        ### Split along the delimiter.
404        _answers = [answer] if not multiple else [a for a in answer.split(delimiter)]
405
406        ### Remove trailing spaces if possible.
407        _answers = [(_a.rstrip(' ') if can_strip_end_spaces else _a) for _a in _answers]
408
409        ### Remove leading spaces if possible.
410        _answers = [(_a.lstrip(' ') if can_strip_start_spaces else _a) for _a in _answers]
411
412        ### Remove empty strings.
413        _answers = [_a for _a in _answers if _a]
414
415        if multiple and len(_answers) == 0:
416            _answers = default_tuple if isinstance(default_tuple, list) else [default_tuple]
417        answers = [altered_choices.get(a, a) for a in _answers]
418
419        valid = (len(answers) > 1 or not (len(answers) == 1 and answers[0] is None))
420        for a in answers:
421            if (
422                not a in {_original for _new, _original in altered_choices.items()}
423                and not a in _choices
424                and a != default
425                and not noask
426            ):
427                valid = False
428                break
429        if valid:
430            break
431        if warn:
432            _warn(f"Please pick a valid choice.", stack=False)
433
434    if not multiple:
435        if not numeric:
436            return answer
437        try:
438            _answer = choices[int(answer) - 1]
439            if as_indices and isinstance(choice, tuple):
440                return _answer[0]
441            return _answer
442        except Exception as e:
443            _warn(f"Could not cast answer '{answer}' to an integer.", stacklevel=3)
444
445    if not numeric:
446        return answers
447    _answers = []
448    for a in answers:
449        try:
450            _answer = choices[int(a) - 1]
451            _answer_to_return = altered_choices.get(_answer, _answer)
452            if isinstance(_answer_to_return, tuple) and as_indices:
453                _answer_to_return = _answer_to_return[0]
454            _answers.append(_answer_to_return)
455        except Exception as e:
456            _warn(f"Could not cast answer '{a}' to an integer.", stacklevel=3)
457    return _answers

Present a list of options and return the user's choice.

Parameters
  • question (str): The question to be printed.
  • choices (List[Union[str, Tuple[str, str]]): A list of options. If an option is a tuple of two strings, the first string is treated as the index and not displayed. In this case, set as_indices to True to return the index.
  • default (Union[str, List[str], None], default None): If the user declines to enter a choice, return this value.
  • numeric (bool, default True): If True, number the items in the list and ask for a number as input. If False, require the user to type the complete string.
  • multiple (bool, default False): If True, allow the user to choose multiple answers separated by delimiter.
  • as_indices (bool, default False): If True, return the indices for the choices. If a choice is a tuple of two strings, the first is assumed to be the index. Otherwise the index in the list is returned.
  • delimiter (str, default ','): If multiple, separate answers by this string. Raise a warning if this string is contained in any of the choices.
  • icon (bool, default True): If True, include the question icon.
  • warn (bool, default True): If True, raise warnings when invalid input is entered.
  • noask (bool, default False): If True, skip printing the question and return the default value.
Returns
  • A string for a single answer or a tuple of strings if multiple is True.
def get_password( username: Optional[str] = None, minimum_length: Optional[int] = None, confirm: bool = True, **kw: Any) -> str:
460def get_password(
461        username: Optional[str] = None,
462        minimum_length: Optional[int] = None,
463        confirm: bool = True,
464        **kw: Any
465    ) -> str:
466    """
467    Prompt the user for a password.
468
469    Parameters
470    ----------
471    username: Optional[str], default None
472        If provided, print the username when asking for a password.
473
474    minimum_length: Optional[int], default None
475        If provided, enforce a password of at least this length.
476
477    confirm: bool, default True
478        If `True`, prompt the user for a password twice.
479
480    Returns
481    -------
482    The password string (censored from terminal output when typing).
483
484    Examples
485    --------
486    ```python-repl
487    >>> get_password()
488     ❓ Password: *******
489     ❓ Confirm password: *******
490    'hunter2'
491    ```
492
493    """
494    from meerschaum.utils.warnings import warn
495    while True:
496        password = prompt(
497            f"Password" + (f" for user '{username}':" if username is not None else ":"),
498            is_password = True,
499            **kw
500        )
501        if minimum_length is not None and len(password) < minimum_length:
502            warn(
503                "Password is too short. " +
504                f"Please enter a password that is at least {minimum_length} characters.",
505                stack = False
506            )
507            continue
508
509        if not confirm:
510            return password
511
512        _password = prompt(
513            f"Confirm password" + (f" for user '{username}':" if username is not None else ":"),
514            is_password = True,
515            **kw
516        )
517        if password != _password:
518            warn(f"Passwords do not match! Please try again.", stack=False)
519            continue
520        else:
521            return password

Prompt the user for a password.

Parameters
  • username (Optional[str], default None): If provided, print the username when asking for a password.
  • minimum_length (Optional[int], default None): If provided, enforce a password of at least this length.
  • confirm (bool, default True): If True, prompt the user for a password twice.
Returns
  • The password string (censored from terminal output when typing).
Examples
>>> get_password()
 ❓ Password: *******
 ❓ Confirm password: *******
'hunter2'
def get_email( username: Optional[str] = None, allow_omit: bool = True, **kw: Any) -> str:
524def get_email(username: Optional[str] = None, allow_omit: bool = True, **kw: Any) -> str:
525    """
526    Prompt the user for an email and enforce that it's valid.
527
528    Parameters
529    ----------
530    username: Optional[str], default None
531        If provided, print the username in the prompt.
532
533    allow_omit: bool, default True
534        If `True`, allow the user to omit the email.
535
536    Returns
537    -------
538    The provided email string.
539
540    Examples
541    --------
542    ```python-repl
543    >>> get_email()
544     ❓ Email (empty to omit): foo@foo
545     Invalid email! Please try again.
546     ❓ Email (empty to omit): foo@foo.com
547    'foo@foo.com'
548    ```
549    """
550    from meerschaum.utils.warnings import warn
551    from meerschaum.utils.misc import is_valid_email
552    while True:
553        email = prompt(
554            f"Email" + (f" for user '{username}'" if username is not None else "") +
555            (" (empty to omit):" if allow_omit else ": "),
556            **kw
557        )
558        if (allow_omit and email == '') or is_valid_email(email):
559            return email
560        warn(f"Invalid email! Please try again.", stack=False)

Prompt the user for an email and enforce that it's valid.

Parameters
  • username (Optional[str], default None): If provided, print the username in the prompt.
  • allow_omit (bool, default True): If True, allow the user to omit the email.
Returns
  • The provided email string.
Examples
>>> get_email()
 ❓ Email (empty to omit): foo@foo
 Invalid email! Please try again.
 ❓ Email (empty to omit): foo@foo.com
'foo@foo.com'
def check_noask(noask: bool = False) -> bool:
563def check_noask(noask: bool = False) -> bool:
564    """
565    Flip `noask` to `True` if `MRSM_NOASK` is set.
566    """
567    from meerschaum.config.static import STATIC_CONFIG
568    NOASK = STATIC_CONFIG['environment']['noask']
569    if noask:
570        return True
571    return (
572        os.environ.get(NOASK, 'false').lower()
573        in ('1', 'true')
574    )

Flip noask to True if MRSM_NOASK is set.