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