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

Flip noask to True if MRSM_NOASK is set.