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