meerschaum 2.2.3__py3-none-any.whl → 2.2.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. meerschaum/__init__.py +4 -1
  2. meerschaum/_internal/arguments/_parse_arguments.py +23 -14
  3. meerschaum/_internal/arguments/_parser.py +4 -2
  4. meerschaum/_internal/docs/index.py +513 -110
  5. meerschaum/_internal/entry.py +2 -4
  6. meerschaum/_internal/shell/Shell.py +0 -3
  7. meerschaum/actions/__init__.py +5 -1
  8. meerschaum/actions/bootstrap.py +32 -7
  9. meerschaum/actions/delete.py +62 -0
  10. meerschaum/actions/edit.py +98 -15
  11. meerschaum/actions/python.py +45 -14
  12. meerschaum/actions/show.py +39 -4
  13. meerschaum/actions/stack.py +12 -12
  14. meerschaum/actions/uninstall.py +24 -29
  15. meerschaum/api/__init__.py +0 -1
  16. meerschaum/api/_oauth2.py +17 -0
  17. meerschaum/api/dash/__init__.py +0 -1
  18. meerschaum/api/dash/callbacks/custom.py +1 -1
  19. meerschaum/api/dash/plugins.py +5 -6
  20. meerschaum/api/routes/_login.py +23 -7
  21. meerschaum/config/__init__.py +16 -6
  22. meerschaum/config/_edit.py +1 -1
  23. meerschaum/config/_paths.py +3 -0
  24. meerschaum/config/_version.py +1 -1
  25. meerschaum/config/stack/__init__.py +3 -1
  26. meerschaum/connectors/Connector.py +7 -2
  27. meerschaum/connectors/__init__.py +7 -5
  28. meerschaum/core/Pipe/_data.py +23 -15
  29. meerschaum/core/Pipe/_deduplicate.py +1 -1
  30. meerschaum/core/Pipe/_dtypes.py +5 -0
  31. meerschaum/core/Pipe/_fetch.py +26 -20
  32. meerschaum/core/Pipe/_sync.py +96 -61
  33. meerschaum/plugins/__init__.py +1 -1
  34. meerschaum/plugins/bootstrap.py +333 -0
  35. meerschaum/utils/daemon/Daemon.py +14 -3
  36. meerschaum/utils/daemon/FileDescriptorInterceptor.py +21 -14
  37. meerschaum/utils/daemon/RotatingFile.py +21 -18
  38. meerschaum/utils/dataframe.py +12 -4
  39. meerschaum/utils/debug.py +9 -15
  40. meerschaum/utils/formatting/__init__.py +23 -10
  41. meerschaum/utils/misc.py +117 -11
  42. meerschaum/utils/packages/_packages.py +1 -0
  43. meerschaum/utils/prompt.py +64 -21
  44. meerschaum/utils/typing.py +1 -0
  45. meerschaum/utils/warnings.py +9 -1
  46. meerschaum/utils/yaml.py +32 -1
  47. {meerschaum-2.2.3.dist-info → meerschaum-2.2.5.dist-info}/METADATA +5 -1
  48. {meerschaum-2.2.3.dist-info → meerschaum-2.2.5.dist-info}/RECORD +54 -53
  49. {meerschaum-2.2.3.dist-info → meerschaum-2.2.5.dist-info}/WHEEL +1 -1
  50. {meerschaum-2.2.3.dist-info → meerschaum-2.2.5.dist-info}/LICENSE +0 -0
  51. {meerschaum-2.2.3.dist-info → meerschaum-2.2.5.dist-info}/NOTICE +0 -0
  52. {meerschaum-2.2.3.dist-info → meerschaum-2.2.5.dist-info}/entry_points.txt +0 -0
  53. {meerschaum-2.2.3.dist-info → meerschaum-2.2.5.dist-info}/top_level.txt +0 -0
  54. {meerschaum-2.2.3.dist-info → meerschaum-2.2.5.dist-info}/zip-safe +0 -0
@@ -65,24 +65,31 @@ class FileDescriptorInterceptor:
65
65
  except BlockingIOError:
66
66
  continue
67
67
  except OSError as e:
68
- continue
68
+ from meerschaum.utils.warnings import warn
69
+ warn(f"OSError in FileDescriptorInterceptor: {e}")
70
+ break
69
71
 
70
- first_char_is_newline = data[0] == b'\n'
71
- last_char_is_newline = data[-1] == b'\n'
72
+ try:
73
+ first_char_is_newline = data[0] == b'\n'
74
+ last_char_is_newline = data[-1] == b'\n'
72
75
 
73
- injected_str = self.injection_hook()
74
- injected_bytes = injected_str.encode('utf-8')
76
+ injected_str = self.injection_hook()
77
+ injected_bytes = injected_str.encode('utf-8')
75
78
 
76
- if is_first_read:
77
- data = b'\n' + data
78
- is_first_read = False
79
+ if is_first_read:
80
+ data = b'\n' + data
81
+ is_first_read = False
79
82
 
80
- modified_data = (
81
- (data[:-1].replace(b'\n', b'\n' + injected_bytes) + b'\n')
82
- if last_char_is_newline
83
- else data.replace(b'\n', b'\n' + injected_bytes)
84
- )
85
- os.write(self.new_file_descriptor, modified_data)
83
+ modified_data = (
84
+ (data[:-1].replace(b'\n', b'\n' + injected_bytes) + b'\n')
85
+ if last_char_is_newline
86
+ else data.replace(b'\n', b'\n' + injected_bytes)
87
+ )
88
+ os.write(self.new_file_descriptor, modified_data)
89
+ except Exception as e:
90
+ from meerschaum.utils.warnings import warn
91
+ warn(f"Error in FileDescriptorInterceptor data processing: {e}")
92
+ break
86
93
 
87
94
 
88
95
  def stop_interception(self):
@@ -355,26 +355,29 @@ class RotatingFile(io.IOBase):
355
355
  As such, if data is larger than max_file_size, then the corresponding subfile
356
356
  may exceed this limit.
357
357
  """
358
- self.file_path.parent.mkdir(exist_ok=True, parents=True)
359
- if isinstance(data, bytes):
360
- data = data.decode('utf-8')
361
-
362
- prefix_str = self.get_timestamp_prefix_str() if self.write_timestamps else ""
363
- suffix_str = "\n" if self.write_timestamps else ""
364
- self.refresh_files(
365
- potential_new_len = len(prefix_str + data + suffix_str),
366
- start_interception = self.write_timestamps,
367
- )
368
358
  try:
369
- if prefix_str:
370
- self._current_file_obj.write(prefix_str)
371
- self._current_file_obj.write(data)
372
- if suffix_str:
373
- self._current_file_obj.write(suffix_str)
359
+ self.file_path.parent.mkdir(exist_ok=True, parents=True)
360
+ if isinstance(data, bytes):
361
+ data = data.decode('utf-8')
362
+
363
+ prefix_str = self.get_timestamp_prefix_str() if self.write_timestamps else ""
364
+ suffix_str = "\n" if self.write_timestamps else ""
365
+ self.refresh_files(
366
+ potential_new_len = len(prefix_str + data + suffix_str),
367
+ start_interception = self.write_timestamps,
368
+ )
369
+ try:
370
+ if prefix_str:
371
+ self._current_file_obj.write(prefix_str)
372
+ self._current_file_obj.write(data)
373
+ if suffix_str:
374
+ self._current_file_obj.write(suffix_str)
375
+ except Exception as e:
376
+ warn(f"Failed to write to subfile:\n{traceback.format_exc()}")
377
+ self.flush()
378
+ self.delete(unused_only=True)
374
379
  except Exception as e:
375
- warn(f"Failed to write to subfile:\n{traceback.format_exc()}")
376
- self.flush()
377
- self.delete(unused_only=True)
380
+ warn(f"Unexpected error in RotatingFile.write: {e}")
378
381
 
379
382
 
380
383
  def delete(self, unused_only: bool = False) -> None:
@@ -8,13 +8,21 @@ Utility functions for working with DataFrames.
8
8
 
9
9
  from __future__ import annotations
10
10
  from datetime import datetime
11
+
12
+ import meerschaum as mrsm
11
13
  from meerschaum.utils.typing import (
12
14
  Optional, Dict, Any, List, Hashable, Generator,
13
- Iterator, Iterable, Union, Tuple,
15
+ Iterator, Iterable, Union, TYPE_CHECKING,
14
16
  )
15
17
 
18
+ if TYPE_CHECKING:
19
+ pd, dask = mrsm.attempt_import('pandas', 'dask')
20
+
16
21
 
17
- def add_missing_cols_to_df(df: 'pd.DataFrame', dtypes: Dict[str, Any]) -> pd.DataFrame:
22
+ def add_missing_cols_to_df(
23
+ df: 'pd.DataFrame',
24
+ dtypes: Dict[str, Any],
25
+ ) -> 'pd.DataFrame':
18
26
  """
19
27
  Add columns from the dtypes dictionary as null columns to a new DataFrame.
20
28
 
@@ -723,7 +731,7 @@ def get_datetime_bound_from_df(
723
731
  df: Union['pd.DataFrame', dict, list],
724
732
  datetime_column: str,
725
733
  minimum: bool = True,
726
- ) -> Union[int, 'datetime.datetime', None]:
734
+ ) -> Union[int, datetime, None]:
727
735
  """
728
736
  Return the minimum or maximum datetime (or integer) from a DataFrame.
729
737
 
@@ -818,7 +826,7 @@ def chunksize_to_npartitions(chunksize: Optional[int]) -> int:
818
826
 
819
827
 
820
828
  def df_from_literal(
821
- pipe: Optional['meerschaum.Pipe'] = None,
829
+ pipe: Optional[mrsm.Pipe] = None,
822
830
  literal: str = None,
823
831
  debug: bool = False
824
832
  ) -> 'pd.DataFrame':
meerschaum/utils/debug.py CHANGED
@@ -93,22 +93,16 @@ def _checkpoint(
93
93
  ) -> None:
94
94
  """If the `_progress` and `_task` objects are provided, increment the task by one step.
95
95
  If `_total` is provided, update the total instead.
96
-
97
- Parameters
98
- ----------
99
- _progress: Optional['rich.progress.Progress'] :
100
- (Default value = None)
101
- _task: Optional[int] :
102
- (Default value = None)
103
- _total: Optional[int] :
104
- (Default value = None)
105
- **kw :
106
-
107
-
108
- Returns
109
- -------
110
-
111
96
  """
112
97
  if _progress is not None and _task is not None:
113
98
  _kw = {'total': _total} if _total is not None else {'advance': 1}
114
99
  _progress.update(_task, **_kw)
100
+
101
+
102
+ def trace(browser: bool = True):
103
+ """
104
+ Open a web-based debugger to trace the execution of the program.
105
+ """
106
+ from meerschaum.utils.packages import attempt_import
107
+ heartrate = attempt_import('heartrate')
108
+ heartrate.trace(files=heartrate.files.all, browser=browser)
@@ -10,7 +10,7 @@ from __future__ import annotations
10
10
  import platform
11
11
  import os
12
12
  import sys
13
- from meerschaum.utils.typing import Optional, Union, Any
13
+ from meerschaum.utils.typing import Optional, Union, Any, Dict
14
14
  from meerschaum.utils.formatting._shell import make_header
15
15
  from meerschaum.utils.formatting._pprint import pprint
16
16
  from meerschaum.utils.formatting._pipes import (
@@ -298,6 +298,7 @@ def print_options(
298
298
  header: Optional[str] = None,
299
299
  num_cols: Optional[int] = None,
300
300
  adjust_cols: bool = True,
301
+ sort_options: bool = False,
301
302
  **kw
302
303
  ) -> None:
303
304
  """
@@ -339,6 +340,8 @@ def print_options(
339
340
  _options = []
340
341
  for o in options:
341
342
  _options.append(str(o))
343
+ if sort_options:
344
+ _options = sorted(_options)
342
345
  _header = f"Available {name}" if header is None else header
343
346
 
344
347
  if num_cols is None:
@@ -349,7 +352,7 @@ def print_options(
349
352
  print()
350
353
  print(make_header(_header))
351
354
  ### print actions
352
- for option in sorted(_options):
355
+ for option in _options:
353
356
  if not nopretty:
354
357
  print(" - ", end="")
355
358
  print(option)
@@ -385,7 +388,7 @@ def print_options(
385
388
 
386
389
  if _header is not None:
387
390
  table = Table(
388
- title = '\n' + _header,
391
+ title = ('\n' + _header) if header else header,
389
392
  box = box.SIMPLE,
390
393
  show_header = False,
391
394
  show_footer = False,
@@ -397,13 +400,22 @@ def print_options(
397
400
  for i in range(num_cols):
398
401
  table.add_column()
399
402
 
400
- chunks = iterate_chunks(
401
- [Text.from_ansi(highlight_pipes(o)) for o in sorted(_options)],
402
- num_cols,
403
- fillvalue=''
404
- )
405
- for c in chunks:
406
- table.add_row(*c)
403
+ if len(_options) < 12:
404
+ # If fewer than 12 items, use a single column
405
+ for option in _options:
406
+ table.add_row(Text.from_ansi(highlight_pipes(option)))
407
+ else:
408
+ # Otherwise, use multiple columns as before
409
+ num_rows = (len(_options) + num_cols - 1) // num_cols
410
+ for i in range(num_rows):
411
+ row = []
412
+ for j in range(num_cols):
413
+ index = i + j * num_rows
414
+ if index < len(_options):
415
+ row.append(Text.from_ansi(highlight_pipes(_options[index])))
416
+ else:
417
+ row.append('')
418
+ table.add_row(*row)
407
419
 
408
420
  get_console().print(table)
409
421
  return None
@@ -455,6 +467,7 @@ def fill_ansi(string: str, style: str = '') -> str:
455
467
 
456
468
  return rich_text_to_str(msg)
457
469
 
470
+
458
471
  def __getattr__(name: str) -> str:
459
472
  """
460
473
  Lazily load module-level variables.
meerschaum/utils/misc.py CHANGED
@@ -6,6 +6,7 @@ Miscellaneous functions go here
6
6
  """
7
7
 
8
8
  from __future__ import annotations
9
+ import sys
9
10
  from datetime import timedelta, datetime, timezone
10
11
  from meerschaum.utils.typing import (
11
12
  Union,
@@ -22,8 +23,11 @@ from meerschaum.utils.typing import (
22
23
  Hashable,
23
24
  Generator,
24
25
  Iterator,
26
+ TYPE_CHECKING,
25
27
  )
26
28
  import meerschaum as mrsm
29
+ if TYPE_CHECKING:
30
+ import collections
27
31
 
28
32
  __pdoc__: Dict[str, bool] = {
29
33
  'to_pandas_dtype': False,
@@ -208,6 +212,7 @@ def parse_config_substitution(
208
212
 
209
213
  return leading_key[len(leading_key):][len():-1].split(delimeter)
210
214
 
215
+
211
216
  def edit_file(
212
217
  path: Union[pathlib.Path, str],
213
218
  default_editor: str = 'pyvim',
@@ -233,7 +238,6 @@ def edit_file(
233
238
  Returns
234
239
  -------
235
240
  A bool indicating the file was successfully edited.
236
-
237
241
  """
238
242
  import os
239
243
  from subprocess import call
@@ -254,7 +258,7 @@ def edit_file(
254
258
 
255
259
 
256
260
  def is_pipe_registered(
257
- pipe: 'meerschaum.Pipe',
261
+ pipe: mrsm.Pipe,
258
262
  pipes: PipesDict,
259
263
  debug: bool = False
260
264
  ) -> bool:
@@ -726,25 +730,53 @@ def replace_password(d: Dict[str, Any], replace_with: str = '*') -> Dict[str, An
726
730
  return _d
727
731
 
728
732
 
733
+ def filter_arguments(
734
+ func: Callable[[Any], Any],
735
+ *args: Any,
736
+ **kwargs: Any
737
+ ) -> Tuple[Tuple[Any], Dict[str, Any]]:
738
+ """
739
+ Filter out unsupported positional and keyword arguments.
740
+
741
+ Parameters
742
+ ----------
743
+ func: Callable[[Any], Any]
744
+ The function to inspect.
745
+
746
+ *args: Any
747
+ Positional arguments to filter and pass to `func`.
748
+
749
+ **kwargs
750
+ Keyword arguments to filter and pass to `func`.
751
+
752
+ Returns
753
+ -------
754
+ The `args` and `kwargs` accepted by `func`.
755
+ """
756
+ args = filter_positionals(func, *args)
757
+ kwargs = filter_keywords(func, **kwargs)
758
+ return args, kwargs
759
+
760
+
729
761
  def filter_keywords(
730
- func: Callable[[Any], Any],
731
- **kw: Any
732
- ) -> Dict[str, Any]:
762
+ func: Callable[[Any], Any],
763
+ **kw: Any
764
+ ) -> Dict[str, Any]:
733
765
  """
734
- Filter out unsupported keywords.
766
+ Filter out unsupported keyword arguments.
735
767
 
736
768
  Parameters
737
769
  ----------
738
770
  func: Callable[[Any], Any]
739
771
  The function to inspect.
740
-
772
+
741
773
  **kw: Any
742
774
  The arguments to be filtered and passed into `func`.
743
775
 
744
776
  Returns
745
777
  -------
746
778
  A dictionary of keyword arguments accepted by `func`.
747
-
779
+
748
780
  Examples
749
781
  --------
750
782
  ```python
@@ -766,6 +798,69 @@ def filter_keywords(
766
798
  return {k: v for k, v in kw.items() if k in func_params}
767
799
 
768
800
 
801
+ def filter_positionals(
802
+ func: Callable[[Any], Any],
803
+ *args: Any
804
+ ) -> Tuple[Any]:
805
+ """
806
+ Filter out unsupported positional arguments.
807
+
808
+ Parameters
809
+ ----------
810
+ func: Callable[[Any], Any]
811
+ The function to inspect.
812
+
813
+ *args: Any
814
+ The arguments to be filtered and passed into `func`.
815
+ NOTE: If the function signature expects more arguments than provided,
816
+ the missing slots will be filled with `None`.
817
+
818
+ Returns
819
+ -------
820
+ A tuple of positional arguments accepted by `func`.
821
+
822
+ Examples
823
+ --------
824
+ ```python
825
+ >>> def foo(a, b):
826
+ ... return a * b
827
+ >>> filter_positionals(foo, 2, 4, 6)
828
+ (2, 4)
829
+ >>> foo(*filter_positionals(foo, 2, 4, 6))
830
+ 8
831
+ ```
832
+
833
+ """
834
+ import inspect
835
+ from meerschaum.utils.warnings import warn
836
+ func_params = inspect.signature(func).parameters
837
+ acceptable_args: List[Any] = []
838
+
839
+ def _warn_invalids(_num_invalid):
840
+ if _num_invalid > 0:
841
+ warn(
842
+ "Too few arguments were provided. "
843
+ + f"{_num_invalid} argument"
844
+ + ('s have ' if _num_invalid != 1 else " has ")
845
+ + " been filled with `None`.",
846
+ )
847
+
848
+ num_invalid: int = 0
849
+ for i, (param, val) in enumerate(func_params.items()):
850
+ if '=' in str(val) or '*' in str(val):
851
+ _warn_invalids(num_invalid)
852
+ return tuple(acceptable_args)
853
+
854
+ try:
855
+ acceptable_args.append(args[i])
856
+ except IndexError:
857
+ acceptable_args.append(None)
858
+ num_invalid += 1
859
+
860
+ _warn_invalids(num_invalid)
861
+ return tuple(acceptable_args)
862
+
863
+
769
864
  def dict_from_od(od: collections.OrderedDict) -> Dict[Any, Any]:
770
865
  """
771
866
  Convert an ordered dict to a dict.
@@ -974,10 +1069,11 @@ def async_wrap(func):
974
1069
  def debug_trace(browser: bool = True):
975
1070
  """
976
1071
  Open a web-based debugger to trace the execution of the program.
1072
+
1073
+ This is an alias import for `meerschaum.utils.debug.debug_trace`.
977
1074
  """
978
- from meerschaum.utils.packages import attempt_import
979
- heartrate = attempt_import('heartrate')
980
- heartrate.trace(files=heartrate.files.all, browser=browser)
1075
+ from meerschaum.utils.debug import trace
1076
+ trace(browser=browser)
981
1077
 
982
1078
 
983
1079
  def items_str(
@@ -1554,3 +1650,13 @@ def _get_subaction_names(*args, **kwargs) -> Any:
1554
1650
  """
1555
1651
  from meerschaum.actions import _get_subaction_names as real_function
1556
1652
  return real_function(*args, **kwargs)
1653
+
1654
+
1655
+ _current_module = sys.modules[__name__]
1656
+ __all__ = tuple(
1657
+ name
1658
+ for name, obj in globals().items()
1659
+ if callable(obj)
1660
+ and name not in __pdoc__
1661
+ and getattr(obj, '__module__', None) == _current_module.__name__
1662
+ )
@@ -42,6 +42,7 @@ packages: Dict[str, Dict[str, str]] = {
42
42
  'requests' : 'requests>=2.23.0',
43
43
  'binaryornot' : 'binaryornot>=0.4.4',
44
44
  'pyvim' : 'pyvim>=3.0.2',
45
+ 'ptpython' : 'ptpython>=3.0.27',
45
46
  'aiofiles' : 'aiofiles>=0.6.0',
46
47
  'packaging' : 'packaging>=21.3.0',
47
48
  'prompt_toolkit' : 'prompt-toolkit>=3.0.39',
@@ -61,6 +61,7 @@ def prompt(
61
61
  from meerschaum.utils.formatting import colored, ANSI, CHARSET, highlight_pipes, fill_ansi
62
62
  from meerschaum.config import get_config
63
63
  from meerschaum.config.static import _static_config
64
+ from meerschaum.utils.misc import filter_keywords
64
65
  noask = check_noask(noask)
65
66
  if not noask:
66
67
  prompt_toolkit = attempt_import('prompt_toolkit')
@@ -101,7 +102,7 @@ def prompt(
101
102
  prompt_toolkit.prompt(
102
103
  prompt_toolkit.formatted_text.ANSI(question),
103
104
  wrap_lines = wrap_lines,
104
- **kw
105
+ **filter_keywords(prompt_toolkit.prompt, **kw)
105
106
  ) if not noask else ''
106
107
  )
107
108
  if noask:
@@ -192,10 +193,11 @@ def yes_no(
192
193
 
193
194
  def choose(
194
195
  question: str,
195
- choices: List[str],
196
- default: Optional[str] = None,
196
+ choices: List[Union[str, Tuple[str, str]]],
197
+ default: Union[str, List[str], None] = None,
197
198
  numeric: bool = True,
198
199
  multiple: bool = False,
200
+ as_indices: bool = False,
199
201
  delimiter: str = ',',
200
202
  icon: bool = True,
201
203
  warn: bool = True,
@@ -210,10 +212,12 @@ def choose(
210
212
  question: str
211
213
  The question to be printed.
212
214
 
213
- choices: List[str]
215
+ choices: List[Union[str, Tuple[str, str]]
214
216
  A list of options.
217
+ If an option is a tuple of two strings, the first string is treated as the index
218
+ and not displayed. In this case, set `as_indices` to `True` to return the index.
215
219
 
216
- default: Optional[str], default None
220
+ default: Union[str, List[str], None], default None
217
221
  If the user declines to enter a choice, return this value.
218
222
 
219
223
  numeric: bool, default True
@@ -223,6 +227,11 @@ def choose(
223
227
  multiple: bool, default False
224
228
  If `True`, allow the user to choose multiple answers separated by `delimiter`.
225
229
 
230
+ as_indices: bool, default False
231
+ If `True`, return the indices for the choices.
232
+ If a choice is a tuple of two strings, the first is assumed to be the index.
233
+ Otherwise the index in the list is returned.
234
+
226
235
  delimiter: str, default ','
227
236
  If `multiple`, separate answers by this string. Raise a warning if this string is contained
228
237
  in any of the choices.
@@ -243,6 +252,7 @@ def choose(
243
252
  """
244
253
  from meerschaum.utils.warnings import warn as _warn
245
254
  from meerschaum.utils.packages import attempt_import
255
+ from meerschaum.utils.misc import print_options
246
256
  noask = check_noask(noask)
247
257
 
248
258
  ### Handle empty choices.
@@ -254,8 +264,14 @@ def choose(
254
264
  if isinstance(default, list):
255
265
  multiple = True
256
266
 
267
+ choices_indices = {}
268
+ for i, c in enumerate(choices):
269
+ if isinstance(c, tuple):
270
+ i, c = c
271
+ choices_indices[i] = c
272
+
257
273
  def _enforce_default(d):
258
- if d is not None and d not in choices and warn:
274
+ if d is not None and d not in choices and d not in choices_indices and warn:
259
275
  _warn(
260
276
  f"Default choice '{default}' is not contained in the choices {choices}. "
261
277
  + "Setting numeric = False.",
@@ -271,16 +287,18 @@ def choose(
271
287
  break
272
288
 
273
289
  _default = default
274
- _choices = choices
290
+ _choices = list(choices_indices.values())
275
291
  if multiple:
276
- question += f"\n Enter your choices, separated by '{delimiter}'."
292
+ question += f"\n Enter your choices, separated by '{delimiter}'.\n"
277
293
 
278
294
  altered_choices = {}
279
295
  altered_indices = {}
280
296
  altered_default_indices = {}
281
297
  delim_replacement = '_' if delimiter != '_' else '-'
282
298
  can_strip_start_spaces, can_strip_end_spaces = True, True
283
- for c in choices:
299
+ for i, c in choices_indices.items():
300
+ if isinstance(c, tuple):
301
+ key, c = c
284
302
  if can_strip_start_spaces and c.startswith(' '):
285
303
  can_strip_start_spaces = False
286
304
  if can_strip_end_spaces and c.endswith(' '):
@@ -301,8 +319,8 @@ def choose(
301
319
  default[i] = new_d
302
320
 
303
321
  ### Check if the choices have the delimiter.
304
- for i, c in enumerate(choices):
305
- if delimiter in c and warn:
322
+ for i, c in choices_indices.items():
323
+ if delimiter in c and not numeric and warn:
306
324
  _warn(
307
325
  f"The delimiter '{delimiter}' is contained within choice '{c}'.\n"
308
326
  + f"Replacing the string '{delimiter}' with '{delim_replacement}' in "
@@ -313,34 +331,53 @@ def choose(
313
331
  altered_choices[new_c] = c
314
332
  altered_indices[i] = new_c
315
333
  for i, new_c in altered_indices.items():
316
- choices[i] = new_c
334
+ choices_indices[i] = new_c
317
335
  default = delimiter.join(default) if isinstance(default, list) else default
318
336
 
337
+ question_options = []
319
338
  if numeric:
320
339
  _choices = [str(i + 1) for i, c in enumerate(choices)]
321
340
  _default = ''
322
341
  if default is not None:
323
342
  for d in (default.split(delimiter) if multiple else [default]):
343
+ if d not in choices and d in choices_indices:
344
+ d_index = d
345
+ d_value = choices_indices[d]
346
+ for _i, _option in enumerate(choices):
347
+ if (
348
+ isinstance(_option, tuple) and (
349
+ _option[1] == d_value
350
+ or
351
+ _option[0] == d_index
352
+ )
353
+ ) or d_index == _i:
354
+ d = _option
355
+
324
356
  _d = str(choices.index(d) + 1)
325
357
  _default += _d + delimiter
326
358
  _default = _default[:-1 * len(delimiter)]
327
- question += '\n'
359
+ # question += '\n'
328
360
  choices_digits = len(str(len(choices)))
329
- for i, c in enumerate(choices):
330
- question += f" {i + 1}. " + (" " * (choices_digits - len(str(i + 1)))) + f"{c}\n"
361
+ for i, c in enumerate(choices_indices.values()):
362
+ question_options.append(
363
+ f" {i + 1}. "
364
+ + (" " * (choices_digits - len(str(i + 1))))
365
+ + f"{c}\n"
366
+ )
331
367
  default_tuple = (_default, default) if default is not None else None
332
368
  else:
333
369
  default_tuple = default
334
- question += '\n'
335
- for c in choices:
336
- question += f" - {c}\n"
370
+ # question += '\n'
371
+ for c in choices_indices.values():
372
+ question_options.append(f"{c}\n")
337
373
 
338
374
  if 'completer' not in kw:
339
375
  WordCompleter = attempt_import('prompt_toolkit.completion').WordCompleter
340
- kw['completer'] = WordCompleter(choices, sentence=True)
376
+ kw['completer'] = WordCompleter(choices_indices.values(), sentence=True)
341
377
 
342
378
  valid = False
343
379
  while not valid:
380
+ print_options(question_options, header='')
344
381
  answer = prompt(
345
382
  question,
346
383
  icon = icon,
@@ -383,7 +420,10 @@ def choose(
383
420
  if not numeric:
384
421
  return answer
385
422
  try:
386
- return choices[int(answer) - 1]
423
+ _answer = choices[int(answer) - 1]
424
+ if as_indices and isinstance(choice, tuple):
425
+ return _answer[0]
426
+ return _answer
387
427
  except Exception as e:
388
428
  _warn(f"Could not cast answer '{answer}' to an integer.", stacklevel=3)
389
429
 
@@ -393,7 +433,10 @@ def choose(
393
433
  for a in answers:
394
434
  try:
395
435
  _answer = choices[int(a) - 1]
396
- _answers.append(altered_choices.get(_answer, _answer))
436
+ _answer_to_return = altered_choices.get(_answer, _answer)
437
+ if isinstance(_answer_to_return, tuple) and as_indices:
438
+ _answer_to_return = _answer_to_return[0]
439
+ _answers.append(_answer_to_return)
397
440
  except Exception as e:
398
441
  _warn(f"Could not cast answer '{a}' to an integer.", stacklevel=3)
399
442
  return _answers
@@ -14,6 +14,7 @@ try:
14
14
  Hashable,
15
15
  Generator,
16
16
  Iterator,
17
+ TYPE_CHECKING,
17
18
  )
18
19
  except Exception as e:
19
20
  import urllib.request, sys, pathlib, os