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.
- meerschaum/__init__.py +4 -1
- meerschaum/_internal/arguments/_parse_arguments.py +23 -14
- meerschaum/_internal/arguments/_parser.py +4 -2
- meerschaum/_internal/docs/index.py +513 -110
- meerschaum/_internal/entry.py +2 -4
- meerschaum/_internal/shell/Shell.py +0 -3
- meerschaum/actions/__init__.py +5 -1
- meerschaum/actions/bootstrap.py +32 -7
- meerschaum/actions/delete.py +62 -0
- meerschaum/actions/edit.py +98 -15
- meerschaum/actions/python.py +45 -14
- meerschaum/actions/show.py +39 -4
- meerschaum/actions/stack.py +12 -12
- meerschaum/actions/uninstall.py +24 -29
- meerschaum/api/__init__.py +0 -1
- meerschaum/api/_oauth2.py +17 -0
- meerschaum/api/dash/__init__.py +0 -1
- meerschaum/api/dash/callbacks/custom.py +1 -1
- meerschaum/api/dash/plugins.py +5 -6
- meerschaum/api/routes/_login.py +23 -7
- meerschaum/config/__init__.py +16 -6
- meerschaum/config/_edit.py +1 -1
- meerschaum/config/_paths.py +3 -0
- meerschaum/config/_version.py +1 -1
- meerschaum/config/stack/__init__.py +3 -1
- meerschaum/connectors/Connector.py +7 -2
- meerschaum/connectors/__init__.py +7 -5
- meerschaum/core/Pipe/_data.py +23 -15
- meerschaum/core/Pipe/_deduplicate.py +1 -1
- meerschaum/core/Pipe/_dtypes.py +5 -0
- meerschaum/core/Pipe/_fetch.py +26 -20
- meerschaum/core/Pipe/_sync.py +96 -61
- meerschaum/plugins/__init__.py +1 -1
- meerschaum/plugins/bootstrap.py +333 -0
- meerschaum/utils/daemon/Daemon.py +14 -3
- meerschaum/utils/daemon/FileDescriptorInterceptor.py +21 -14
- meerschaum/utils/daemon/RotatingFile.py +21 -18
- meerschaum/utils/dataframe.py +12 -4
- meerschaum/utils/debug.py +9 -15
- meerschaum/utils/formatting/__init__.py +23 -10
- meerschaum/utils/misc.py +117 -11
- meerschaum/utils/packages/_packages.py +1 -0
- meerschaum/utils/prompt.py +64 -21
- meerschaum/utils/typing.py +1 -0
- meerschaum/utils/warnings.py +9 -1
- meerschaum/utils/yaml.py +32 -1
- {meerschaum-2.2.3.dist-info → meerschaum-2.2.5.dist-info}/METADATA +5 -1
- {meerschaum-2.2.3.dist-info → meerschaum-2.2.5.dist-info}/RECORD +54 -53
- {meerschaum-2.2.3.dist-info → meerschaum-2.2.5.dist-info}/WHEEL +1 -1
- {meerschaum-2.2.3.dist-info → meerschaum-2.2.5.dist-info}/LICENSE +0 -0
- {meerschaum-2.2.3.dist-info → meerschaum-2.2.5.dist-info}/NOTICE +0 -0
- {meerschaum-2.2.3.dist-info → meerschaum-2.2.5.dist-info}/entry_points.txt +0 -0
- {meerschaum-2.2.3.dist-info → meerschaum-2.2.5.dist-info}/top_level.txt +0 -0
- {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
|
-
|
68
|
+
from meerschaum.utils.warnings import warn
|
69
|
+
warn(f"OSError in FileDescriptorInterceptor: {e}")
|
70
|
+
break
|
69
71
|
|
70
|
-
|
71
|
-
|
72
|
+
try:
|
73
|
+
first_char_is_newline = data[0] == b'\n'
|
74
|
+
last_char_is_newline = data[-1] == b'\n'
|
72
75
|
|
73
|
-
|
74
|
-
|
76
|
+
injected_str = self.injection_hook()
|
77
|
+
injected_bytes = injected_str.encode('utf-8')
|
75
78
|
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
+
if is_first_read:
|
80
|
+
data = b'\n' + data
|
81
|
+
is_first_read = False
|
79
82
|
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
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
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
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"
|
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:
|
meerschaum/utils/dataframe.py
CHANGED
@@ -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,
|
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(
|
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,
|
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[
|
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
|
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
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
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:
|
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
|
-
|
731
|
-
|
732
|
-
|
762
|
+
func: Callable[[Any], Any],
|
763
|
+
**kw: Any
|
764
|
+
) -> Dict[str, Any]:
|
733
765
|
"""
|
734
|
-
Filter out unsupported
|
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.
|
979
|
-
|
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',
|
meerschaum/utils/prompt.py
CHANGED
@@ -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:
|
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:
|
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 =
|
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
|
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
|
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
|
-
|
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(
|
330
|
-
|
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
|
336
|
-
|
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(
|
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
|
-
|
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
|
-
|
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
|