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

Flip noask to True if MRSM_NOASK is set.

def get_connectors_completer(*types: str):
609def get_connectors_completer(*types: str):
610    """
611    Return a prompt-toolkit Completer object to pass into `prompt()`.
612    """
613    from meerschaum.utils.misc import get_connector_labels
614    prompt_toolkit_completion = mrsm.attempt_import('prompt_toolkit.completion', lazy=False)
615    Completer = prompt_toolkit_completion.Completer
616    Completion = prompt_toolkit_completion.Completion
617
618    class ConnectorCompleter(Completer):
619        def get_completions(self, document, complete_event):
620            for label in get_connector_labels(*types, search_term=document.text):
621                yield Completion(label, start_position=(-1 * len(document.text)))
622
623    return ConnectorCompleter()

Return a prompt-toolkit Completer object to pass into prompt().