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

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

Parameters
  • question (str): The question to be printed.
  • choices (List[str]): A list of options.
  • default (Optional[str], 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.
  • 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:
403def get_password(
404        username: Optional[str] = None,
405        minimum_length: Optional[int] = None,
406        confirm: bool = True,
407        **kw: Any
408    ) -> str:
409    """
410    Prompt the user for a password.
411
412    Parameters
413    ----------
414    username: Optional[str], default None
415        If provided, print the username when asking for a password.
416
417    minimum_length: Optional[int], default None
418        If provided, enforce a password of at least this length.
419
420    confirm: bool, default True
421        If `True`, prompt the user for a password twice.
422
423    Returns
424    -------
425    The password string (censored from terminal output when typing).
426
427    Examples
428    --------
429    ```python-repl
430    >>> get_password()
431     ❓ Password: *******
432     ❓ Confirm password: *******
433    'hunter2'
434    ```
435
436    """
437    from meerschaum.utils.warnings import warn
438    while True:
439        password = prompt(
440            f"Password" + (f" for user '{username}':" if username is not None else ":"),
441            is_password = True,
442            **kw
443        )
444        if minimum_length is not None and len(password) < minimum_length:
445            warn(
446                "Password is too short. " +
447                f"Please enter a password that is at least {minimum_length} characters.",
448                stack = False
449            )
450            continue
451
452        if not confirm:
453            return password
454
455        _password = prompt(
456            f"Confirm password" + (f" for user '{username}':" if username is not None else ":"),
457            is_password = True,
458            **kw
459        )
460        if password != _password:
461            warn(f"Passwords do not match! Please try again.", stack=False)
462            continue
463        else:
464            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:
467def get_email(username: Optional[str] = None, allow_omit: bool = True, **kw: Any) -> str:
468    """
469    Prompt the user for an email and enforce that it's valid.
470
471    Parameters
472    ----------
473    username: Optional[str], default None
474        If provided, print the username in the prompt.
475
476    allow_omit: bool, default True
477        If `True`, allow the user to omit the email.
478
479    Returns
480    -------
481    The provided email string.
482
483    Examples
484    --------
485    ```python-repl
486    >>> get_email()
487     ❓ Email (empty to omit): foo@foo
488     Invalid email! Please try again.
489     ❓ Email (empty to omit): foo@foo.com
490    'foo@foo.com'
491    ```
492    """
493    from meerschaum.utils.warnings import warn
494    from meerschaum.utils.misc import is_valid_email
495    while True:
496        email = prompt(
497            f"Email" + (f" for user '{username}'" if username is not None else "") +
498            (" (empty to omit):" if allow_omit else ": "),
499            **kw
500        )
501        if (allow_omit and email == '') or is_valid_email(email):
502            return email
503        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:
506def check_noask(noask: bool = False) -> bool:
507    """
508    Flip `noask` to `True` if `MRSM_NOASK` is set.
509    """
510    from meerschaum.config.static import STATIC_CONFIG
511    NOASK = STATIC_CONFIG['environment']['noask']
512    if noask:
513        return True
514    return (
515        os.environ.get(NOASK, 'false').lower()
516        in ('1', 'true')
517    )

Flip noask to True if MRSM_NOASK is set.