ask-shell 0.10.2__tar.gz → 0.11.0__tar.gz
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.
- {ask_shell-0.10.2 → ask_shell-0.11.0}/PKG-INFO +2 -2
- {ask_shell-0.10.2 → ask_shell-0.11.0}/ask_shell/__init__.py +3 -3
- {ask_shell-0.10.2 → ask_shell-0.11.0}/ask_shell/_internal/_run.py +18 -19
- {ask_shell-0.10.2 → ask_shell-0.11.0}/ask_shell/_internal/interactive.py +156 -92
- ask_shell-0.11.0/ask_shell/_internal/interactive_models.py +87 -0
- {ask_shell-0.10.2 → ask_shell-0.11.0}/ask_shell/_internal/models.py +4 -5
- ask_shell-0.11.0/ask_shell/_internal/non_interactive.py +268 -0
- {ask_shell-0.10.2 → ask_shell-0.11.0}/ask_shell/_internal/rich_live.py +5 -6
- {ask_shell-0.10.2 → ask_shell-0.11.0}/ask_shell/_internal/rich_progress.py +2 -4
- {ask_shell-0.10.2 → ask_shell-0.11.0}/ask_shell/_internal/run_pool.py +1 -1
- {ask_shell-0.10.2 → ask_shell-0.11.0}/ask_shell/_internal/typer_command.py +43 -8
- {ask_shell-0.10.2 → ask_shell-0.11.0}/ask_shell/ask.py +6 -2
- {ask_shell-0.10.2 → ask_shell-0.11.0}/ask_shell/settings.py +73 -7
- {ask_shell-0.10.2 → ask_shell-0.11.0}/ask_shell/test_docs.py +1 -1
- {ask_shell-0.10.2 → ask_shell-0.11.0}/pyproject.toml +9 -2
- {ask_shell-0.10.2 → ask_shell-0.11.0}/.gitignore +0 -0
- {ask_shell-0.10.2 → ask_shell-0.11.0}/LICENSE +0 -0
- {ask_shell-0.10.2 → ask_shell-0.11.0}/README.md +0 -0
- {ask_shell-0.10.2 → ask_shell-0.11.0}/ask_shell/__main__.py +0 -0
- {ask_shell-0.10.2 → ask_shell-0.11.0}/ask_shell/_internal/__init__.py +0 -0
- {ask_shell-0.10.2 → ask_shell-0.11.0}/ask_shell/_internal/_run_env.py +0 -0
- {ask_shell-0.10.2 → ask_shell-0.11.0}/ask_shell/_internal/events.py +0 -0
- {ask_shell-0.10.2 → ask_shell-0.11.0}/ask_shell/_internal/global_callbacks.py +0 -0
- {ask_shell-0.10.2 → ask_shell-0.11.0}/ask_shell/_internal/live_print_context.py +0 -0
- {ask_shell-0.10.2 → ask_shell-0.11.0}/ask_shell/_internal/rich_live_callback.py +0 -0
- {ask_shell-0.10.2 → ask_shell-0.11.0}/ask_shell/_internal/rich_run_state.py +0 -0
- {ask_shell-0.10.2 → ask_shell-0.11.0}/ask_shell/console.py +0 -0
- {ask_shell-0.10.2 → ask_shell-0.11.0}/ask_shell/py.typed +0 -0
- {ask_shell-0.10.2 → ask_shell-0.11.0}/ask_shell/shell.py +0 -0
- {ask_shell-0.10.2 → ask_shell-0.11.0}/ask_shell/shell_events.py +0 -0
- {ask_shell-0.10.2 → ask_shell-0.11.0}/examples/apply_live/__init__.py +0 -0
- {ask_shell-0.10.2 → ask_shell-0.11.0}/examples/apply_live/cli.py +0 -0
- {ask_shell-0.10.2 → ask_shell-0.11.0}/examples/apply_live/stream_handler.py +0 -0
- {ask_shell-0.10.2 → ask_shell-0.11.0}/scripts/fix_source_links.py +0 -0
|
@@ -3,16 +3,16 @@
|
|
|
3
3
|
from ask_shell import shell
|
|
4
4
|
from ask_shell import console
|
|
5
5
|
from ask_shell import shell_events
|
|
6
|
-
from ask_shell import ask
|
|
7
6
|
from ask_shell.settings import AskShellSettings
|
|
8
7
|
from ask_shell.settings import ShellRunSummary
|
|
8
|
+
from ask_shell import ask
|
|
9
9
|
|
|
10
|
-
VERSION = "0.
|
|
10
|
+
VERSION = "0.11.0"
|
|
11
11
|
__all__ = [
|
|
12
12
|
"shell",
|
|
13
13
|
"console",
|
|
14
14
|
"shell_events",
|
|
15
|
-
"ask",
|
|
16
15
|
"AskShellSettings",
|
|
17
16
|
"ShellRunSummary",
|
|
17
|
+
"ask",
|
|
18
18
|
]
|
|
@@ -278,16 +278,13 @@ def _run(
|
|
|
278
278
|
output_dir: Path,
|
|
279
279
|
file_name: str,
|
|
280
280
|
) -> None:
|
|
281
|
-
kwargs =
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
| config.popen_kwargs
|
|
289
|
-
)
|
|
290
|
-
with subprocess.Popen(config.shell_input, shell=True, **kwargs) as proc: # type: ignore
|
|
281
|
+
kwargs = {
|
|
282
|
+
"stdout": subprocess.PIPE,
|
|
283
|
+
"stderr": subprocess.PIPE,
|
|
284
|
+
"start_new_session": True,
|
|
285
|
+
"universal_newlines": True,
|
|
286
|
+
} | config.popen_kwargs
|
|
287
|
+
with subprocess.Popen(config.shell_input, shell=True, **kwargs) as proc:
|
|
291
288
|
queue.put_nowait(ShellRunPOpenStarted(proc))
|
|
292
289
|
|
|
293
290
|
def stdout_started(is_stdout: bool, console: Console, log_path: Path):
|
|
@@ -310,7 +307,7 @@ def _run(
|
|
|
310
307
|
is_stdout=True,
|
|
311
308
|
)
|
|
312
309
|
except BaseException as e:
|
|
313
|
-
logger.exception(
|
|
310
|
+
logger.exception("stdout read failed")
|
|
314
311
|
queue.put_nowait(ShellRunStdReadError(is_stdout=True, error=e))
|
|
315
312
|
|
|
316
313
|
def add_stderr_line(line: str):
|
|
@@ -330,7 +327,7 @@ def _run(
|
|
|
330
327
|
is_stdout=False,
|
|
331
328
|
)
|
|
332
329
|
except BaseException as e:
|
|
333
|
-
logger.exception(
|
|
330
|
+
logger.exception("stderr read failed")
|
|
334
331
|
queue.put_nowait(ShellRunStdReadError(is_stdout=False, error=e))
|
|
335
332
|
|
|
336
333
|
fut_stdout = _pool.submit(read_stdout)
|
|
@@ -357,7 +354,7 @@ def _attempt_run(
|
|
|
357
354
|
return shell_run
|
|
358
355
|
except BaseException as e:
|
|
359
356
|
logger.warning(f"Error running {shell_run}: {e!r}")
|
|
360
|
-
logger.exception(
|
|
357
|
+
logger.exception("run failed")
|
|
361
358
|
return e
|
|
362
359
|
finally:
|
|
363
360
|
_runs.pop(key, None)
|
|
@@ -369,7 +366,7 @@ def _eval_should_retry(config: ShellConfig, result: ShellRun) -> bool:
|
|
|
369
366
|
return config.should_retry(result)
|
|
370
367
|
except AbortRetryError:
|
|
371
368
|
raise
|
|
372
|
-
except
|
|
369
|
+
except (TypeError, ValueError, RuntimeError, OSError, AttributeError) as e:
|
|
373
370
|
logger.warning(f"should_retry callback failed: {e!r}")
|
|
374
371
|
return False
|
|
375
372
|
|
|
@@ -423,13 +420,13 @@ def _execute_run(shell_run: ShellRun) -> ShellRun:
|
|
|
423
420
|
ctx.run(shell_run._on_event, message)
|
|
424
421
|
except BaseException as e:
|
|
425
422
|
logger.warning(f"Error processing message '{type(message).__name__}' for {shell_run}: {e!r}")
|
|
426
|
-
logger.exception(
|
|
423
|
+
logger.exception("message callback failed")
|
|
427
424
|
|
|
428
425
|
try:
|
|
429
426
|
shell_run._on_event(
|
|
430
427
|
ShellRunBefore(run=shell_run)
|
|
431
428
|
) # can block, for example if the thread pool doesn't have enough threads free
|
|
432
|
-
except BaseException as e:
|
|
429
|
+
except BaseException as e: # noqa: BLE001
|
|
433
430
|
logger.warning(f"Error before starting run {shell_run}: {e!r}")
|
|
434
431
|
shell_run._complete(error=e)
|
|
435
432
|
return shell_run
|
|
@@ -466,7 +463,7 @@ def run(
|
|
|
466
463
|
retry_jitter: float | None = None,
|
|
467
464
|
retry_max_wait: float | None = None,
|
|
468
465
|
run_log_stem_prefix: str | None = None,
|
|
469
|
-
run_output_dir: Path | None
|
|
466
|
+
run_output_dir: Path | None = None,
|
|
470
467
|
settings: AskShellSettings | None = None,
|
|
471
468
|
should_retry: Callable[[ShellRun], bool] | None = None,
|
|
472
469
|
skip_binary_check: bool | None = None,
|
|
@@ -533,7 +530,7 @@ def run_and_wait(
|
|
|
533
530
|
retry_jitter: float | None = None,
|
|
534
531
|
retry_max_wait: float | None = None,
|
|
535
532
|
run_log_stem_prefix: str | None = None,
|
|
536
|
-
run_output_dir: Path | None
|
|
533
|
+
run_output_dir: Path | None = None,
|
|
537
534
|
settings: AskShellSettings | None = None,
|
|
538
535
|
should_retry: Callable[[ShellRun], bool] | None = None,
|
|
539
536
|
skip_binary_check: bool | None = None,
|
|
@@ -586,7 +583,9 @@ def run_and_wait(
|
|
|
586
583
|
def run_error(run: ShellRun, timeout: float | None = 1) -> BaseException | None:
|
|
587
584
|
try:
|
|
588
585
|
run._complete_flag.result(timeout=timeout)
|
|
589
|
-
except
|
|
586
|
+
except TimeoutError as e:
|
|
587
|
+
return e
|
|
588
|
+
except Exception as e: # noqa: BLE001
|
|
590
589
|
return e
|
|
591
590
|
|
|
592
591
|
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
"""Inspired by: https://github.com/tmbo/questionary/blob/master/tests/utils.py"""
|
|
2
|
-
|
|
3
1
|
from __future__ import annotations
|
|
4
2
|
|
|
5
3
|
import asyncio
|
|
@@ -7,25 +5,31 @@ import io
|
|
|
7
5
|
import logging
|
|
8
6
|
import os
|
|
9
7
|
import string
|
|
8
|
+
from collections.abc import Callable, Sequence
|
|
10
9
|
from dataclasses import dataclass, field
|
|
11
|
-
from
|
|
12
|
-
from typing import Any, Callable, Generic, Self, TypeVar
|
|
10
|
+
from typing import Any, Generic, Self, TypeVar
|
|
13
11
|
|
|
14
12
|
from prompt_toolkit.input.defaults import create_pipe_input
|
|
15
13
|
from prompt_toolkit.output import DummyOutput
|
|
16
14
|
from prompt_toolkit.output.plain_text import PlainTextOutput
|
|
17
15
|
from pydantic import BaseModel, model_validator
|
|
18
|
-
from questionary import
|
|
16
|
+
from questionary import Question, checkbox
|
|
19
17
|
from questionary import confirm as _confirm
|
|
20
18
|
from questionary import select as _select
|
|
21
19
|
from questionary import text as _text
|
|
22
|
-
from zero_3rdparty.object_name import as_name
|
|
20
|
+
from zero_3rdparty.object_name import as_name
|
|
23
21
|
from zero_3rdparty.str_utils import ensure_suffix
|
|
24
22
|
|
|
25
23
|
from ask_shell._internal._run import get_pool
|
|
26
24
|
from ask_shell._internal._run_env import (
|
|
27
25
|
interactive_shell,
|
|
28
26
|
)
|
|
27
|
+
from ask_shell._internal.interactive_models import ChoiceTyped, PromptKind
|
|
28
|
+
from ask_shell._internal.non_interactive import (
|
|
29
|
+
record_answered_row,
|
|
30
|
+
replay_or_dump,
|
|
31
|
+
try_replay_answered,
|
|
32
|
+
)
|
|
29
33
|
from ask_shell._internal.rich_live import pause_live
|
|
30
34
|
from ask_shell.settings import AskShellSettings, _global_settings
|
|
31
35
|
|
|
@@ -48,29 +52,6 @@ def _default_asker(q: Question, _: type[T]) -> T:
|
|
|
48
52
|
_question_asker: TypedAsk = _default_asker
|
|
49
53
|
|
|
50
54
|
|
|
51
|
-
FuncT = TypeVar("FuncT", bound=Callable)
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def return_default_if_not_interactive(func: FuncT) -> FuncT:
|
|
55
|
-
assert "default" in func_arg_names(func), f"Function {as_name(func)} must have a 'default' parameter"
|
|
56
|
-
|
|
57
|
-
@wraps(func)
|
|
58
|
-
def return_default(*args, **kwargs):
|
|
59
|
-
if interactive_shell():
|
|
60
|
-
return func(*args, **kwargs)
|
|
61
|
-
default_value = kwargs.get("default", None)
|
|
62
|
-
if default_value is None:
|
|
63
|
-
raise ValueError(
|
|
64
|
-
f"Function called in non-interactive shell, but no default value provided &func={as_name(func)}"
|
|
65
|
-
)
|
|
66
|
-
logger.warning(
|
|
67
|
-
f"Function {as_name(func)} called in non-interactive shell, returning default value: {default_value}"
|
|
68
|
-
)
|
|
69
|
-
return default_value
|
|
70
|
-
|
|
71
|
-
return return_default # type: ignore
|
|
72
|
-
|
|
73
|
-
|
|
74
55
|
_PROMPT_TEXT_ATTR_NAME = "__prompt_text__"
|
|
75
56
|
|
|
76
57
|
|
|
@@ -78,16 +59,6 @@ def _set_prompt_text(q: Question, prompt_text: str) -> None:
|
|
|
78
59
|
setattr(q, _PROMPT_TEXT_ATTR_NAME, prompt_text)
|
|
79
60
|
|
|
80
61
|
|
|
81
|
-
@pause_live
|
|
82
|
-
def confirm(prompt_text: str, *, default: bool | None = None) -> bool:
|
|
83
|
-
if default is None:
|
|
84
|
-
question = _confirm(prompt_text)
|
|
85
|
-
else:
|
|
86
|
-
question = _confirm(prompt_text, default=default)
|
|
87
|
-
_set_prompt_text(question, prompt_text)
|
|
88
|
-
return _question_asker(question, bool)
|
|
89
|
-
|
|
90
|
-
|
|
91
62
|
_unset = object()
|
|
92
63
|
|
|
93
64
|
|
|
@@ -103,38 +74,78 @@ class NewHandlerChoice(Generic[T]):
|
|
|
103
74
|
new_prompt: str
|
|
104
75
|
|
|
105
76
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
77
|
+
def _ask_with_session(
|
|
78
|
+
*,
|
|
79
|
+
kind: PromptKind,
|
|
80
|
+
prompt: str,
|
|
81
|
+
choices: Sequence[ChoiceTyped],
|
|
82
|
+
has_usable_default: bool,
|
|
83
|
+
usable_default: Any,
|
|
84
|
+
ask_fn: Callable[[], T],
|
|
85
|
+
) -> T:
|
|
86
|
+
if _question_asker is not _default_asker:
|
|
87
|
+
return ask_fn()
|
|
88
|
+
settings = AskShellSettings.from_env()
|
|
89
|
+
if not interactive_shell():
|
|
90
|
+
if settings.use_defaults and has_usable_default:
|
|
91
|
+
logger.warning(f"Ask called in non-interactive shell, returning default value: {usable_default}")
|
|
92
|
+
record_answered_row(
|
|
93
|
+
kind=kind,
|
|
94
|
+
prompt=prompt,
|
|
95
|
+
settings=settings,
|
|
96
|
+
choices=choices,
|
|
97
|
+
value=usable_default,
|
|
98
|
+
)
|
|
99
|
+
return usable_default
|
|
100
|
+
return replay_or_dump(kind=kind, prompt=prompt, settings=settings, choices=choices)
|
|
101
|
+
replayed = try_replay_answered(kind=kind, prompt=prompt, settings=settings, choices=choices)
|
|
102
|
+
if replayed is not None:
|
|
103
|
+
return replayed
|
|
104
|
+
value = ask_fn()
|
|
105
|
+
record_answered_row(kind=kind, prompt=prompt, settings=settings, choices=choices, value=value)
|
|
106
|
+
return value
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@pause_live
|
|
110
|
+
def confirm(prompt_text: str, *, default: bool | None = None) -> bool:
|
|
111
|
+
def ask_fn() -> bool:
|
|
112
|
+
if default is None:
|
|
113
|
+
question = _confirm(prompt_text)
|
|
114
|
+
else:
|
|
115
|
+
question = _confirm(prompt_text, default=default)
|
|
116
|
+
_set_prompt_text(question, prompt_text)
|
|
117
|
+
return _question_asker(question, bool)
|
|
118
|
+
|
|
119
|
+
return _ask_with_session(
|
|
120
|
+
kind=PromptKind.CONFIRM,
|
|
121
|
+
prompt=prompt_text,
|
|
122
|
+
choices=(),
|
|
123
|
+
has_usable_default=default is not None,
|
|
124
|
+
usable_default=default,
|
|
125
|
+
ask_fn=ask_fn,
|
|
126
|
+
)
|
|
127
127
|
|
|
128
128
|
|
|
129
129
|
@pause_live
|
|
130
|
-
@return_default_if_not_interactive
|
|
131
130
|
def text(
|
|
132
131
|
prompt_text: str,
|
|
133
|
-
default: str =
|
|
132
|
+
default: str | None = None,
|
|
134
133
|
) -> str:
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
134
|
+
questionary_default = default if default is not None else ""
|
|
135
|
+
|
|
136
|
+
def ask_fn() -> str:
|
|
137
|
+
question = _text(prompt_text, default=questionary_default)
|
|
138
|
+
_set_prompt_text(question, prompt_text)
|
|
139
|
+
return _question_asker(question, str)
|
|
140
|
+
|
|
141
|
+
return _ask_with_session(
|
|
142
|
+
kind=PromptKind.TEXT,
|
|
143
|
+
prompt=prompt_text,
|
|
144
|
+
choices=(),
|
|
145
|
+
has_usable_default=default is not None,
|
|
146
|
+
usable_default=default,
|
|
147
|
+
ask_fn=ask_fn,
|
|
148
|
+
)
|
|
138
149
|
|
|
139
150
|
|
|
140
151
|
class SelectOptions(BaseModel, Generic[T]):
|
|
@@ -194,7 +205,7 @@ class SelectOptions(BaseModel, Generic[T]):
|
|
|
194
205
|
)
|
|
195
206
|
_set_prompt_text(question, prompt_text)
|
|
196
207
|
try:
|
|
197
|
-
return _question_asker(question, T)
|
|
208
|
+
return _question_asker(question, T) # ty: ignore[invalid-argument-type]
|
|
198
209
|
except KeyboardInterrupt:
|
|
199
210
|
if not self.allow_new:
|
|
200
211
|
raise
|
|
@@ -228,7 +239,6 @@ class SelectOptions(BaseModel, Generic[T]):
|
|
|
228
239
|
|
|
229
240
|
|
|
230
241
|
@pause_live
|
|
231
|
-
@return_default_if_not_interactive
|
|
232
242
|
def select_list_multiple(
|
|
233
243
|
prompt_text: str,
|
|
234
244
|
choices: list[str],
|
|
@@ -238,13 +248,23 @@ def select_list_multiple(
|
|
|
238
248
|
) -> list[str]:
|
|
239
249
|
assert choices, f"choices must not be empty for {as_name(select_list_multiple)}"
|
|
240
250
|
options = options or SelectOptions()
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
251
|
+
default_checked = default or []
|
|
252
|
+
typed_choices = [ChoiceTyped(option, checked=option in default_checked, value=option) for option in choices]
|
|
253
|
+
|
|
254
|
+
def ask_fn() -> list[str]:
|
|
255
|
+
return options._select_multiple(prompt_text, typed_choices)
|
|
256
|
+
|
|
257
|
+
return _ask_with_session(
|
|
258
|
+
kind=PromptKind.SELECT_MULTIPLE,
|
|
259
|
+
prompt=prompt_text,
|
|
260
|
+
choices=typed_choices,
|
|
261
|
+
has_usable_default=default is not None,
|
|
262
|
+
usable_default=default_checked,
|
|
263
|
+
ask_fn=ask_fn,
|
|
264
|
+
)
|
|
244
265
|
|
|
245
266
|
|
|
246
267
|
@pause_live
|
|
247
|
-
@return_default_if_not_interactive
|
|
248
268
|
def select_list_multiple_choices(
|
|
249
269
|
prompt_text: str,
|
|
250
270
|
choices: list[ChoiceTyped[T]],
|
|
@@ -255,11 +275,21 @@ def select_list_multiple_choices(
|
|
|
255
275
|
) -> list[T]:
|
|
256
276
|
assert choices, f"choices must not be empty for {as_name(select_list_multiple_choices)}"
|
|
257
277
|
options = options or SelectOptions()
|
|
258
|
-
|
|
278
|
+
|
|
279
|
+
def ask_fn() -> list[T]:
|
|
280
|
+
return options._select_multiple(prompt_text, choices)
|
|
281
|
+
|
|
282
|
+
return _ask_with_session(
|
|
283
|
+
kind=PromptKind.SELECT_MULTIPLE,
|
|
284
|
+
prompt=prompt_text,
|
|
285
|
+
choices=choices,
|
|
286
|
+
has_usable_default=default is not None,
|
|
287
|
+
usable_default=default or [],
|
|
288
|
+
ask_fn=ask_fn,
|
|
289
|
+
)
|
|
259
290
|
|
|
260
291
|
|
|
261
292
|
@pause_live
|
|
262
|
-
@return_default_if_not_interactive
|
|
263
293
|
def select_dict(
|
|
264
294
|
prompt_text: str,
|
|
265
295
|
choices: dict[str, T],
|
|
@@ -270,12 +300,23 @@ def select_dict(
|
|
|
270
300
|
assert choices, f"choices must not be empty for {as_name(select_dict)}"
|
|
271
301
|
options = options or SelectOptions()
|
|
272
302
|
default_safe = default or ""
|
|
273
|
-
|
|
274
|
-
|
|
303
|
+
typed_choices = [ChoiceTyped(name=key, value=value, checked=key == default_safe) for key, value in choices.items()]
|
|
304
|
+
default_in_choices = default in choices if default is not None else False
|
|
305
|
+
|
|
306
|
+
def ask_fn() -> T:
|
|
307
|
+
return options._select(prompt_text, typed_choices)
|
|
308
|
+
|
|
309
|
+
return _ask_with_session(
|
|
310
|
+
kind=PromptKind.SELECT,
|
|
311
|
+
prompt=prompt_text,
|
|
312
|
+
choices=typed_choices,
|
|
313
|
+
has_usable_default=default_in_choices,
|
|
314
|
+
usable_default=choices[default] if default is not None and default in choices else None,
|
|
315
|
+
ask_fn=ask_fn,
|
|
316
|
+
)
|
|
275
317
|
|
|
276
318
|
|
|
277
319
|
@pause_live
|
|
278
|
-
@return_default_if_not_interactive
|
|
279
320
|
def select_list(
|
|
280
321
|
prompt_text: str,
|
|
281
322
|
choices: list[str],
|
|
@@ -285,22 +326,34 @@ def select_list(
|
|
|
285
326
|
) -> str:
|
|
286
327
|
assert choices, f"choices must not be empty for {as_name(select_list)}"
|
|
287
328
|
options = options or SelectOptions()
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
329
|
+
typed_choices = [ChoiceTyped(name=choice, value=choice) for choice in choices]
|
|
330
|
+
default_in_choices = default in choices if default is not None else False
|
|
331
|
+
|
|
332
|
+
def ask_fn() -> str:
|
|
333
|
+
chosen = options.set_defaults(len(choices))
|
|
334
|
+
return _question_asker(
|
|
335
|
+
_select(
|
|
336
|
+
prompt_text,
|
|
337
|
+
default=default,
|
|
338
|
+
choices=choices,
|
|
339
|
+
use_jk_keys=chosen.use_jk_keys,
|
|
340
|
+
use_shortcuts=chosen.use_shortcuts,
|
|
341
|
+
use_search_filter=chosen.use_search_filter,
|
|
342
|
+
),
|
|
343
|
+
str,
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
return _ask_with_session(
|
|
347
|
+
kind=PromptKind.SELECT,
|
|
348
|
+
prompt=prompt_text,
|
|
349
|
+
choices=typed_choices,
|
|
350
|
+
has_usable_default=default_in_choices,
|
|
351
|
+
usable_default=default if default_in_choices else None,
|
|
352
|
+
ask_fn=ask_fn,
|
|
299
353
|
)
|
|
300
354
|
|
|
301
355
|
|
|
302
356
|
@pause_live
|
|
303
|
-
@return_default_if_not_interactive
|
|
304
357
|
def select_list_choice(
|
|
305
358
|
prompt_text: str,
|
|
306
359
|
choices: list[ChoiceTyped[T]],
|
|
@@ -310,7 +363,20 @@ def select_list_choice(
|
|
|
310
363
|
) -> T:
|
|
311
364
|
assert choices, f"choices must not be empty for {as_name(select_list_choice)}"
|
|
312
365
|
options = options or SelectOptions()
|
|
313
|
-
|
|
366
|
+
choice_values = {choice.value for choice in choices}
|
|
367
|
+
default_in_choices = default is not None and default in choice_values
|
|
368
|
+
|
|
369
|
+
def ask_fn() -> T:
|
|
370
|
+
return options._select(prompt_text, choices)
|
|
371
|
+
|
|
372
|
+
return _ask_with_session(
|
|
373
|
+
kind=PromptKind.SELECT,
|
|
374
|
+
prompt=prompt_text,
|
|
375
|
+
choices=choices,
|
|
376
|
+
has_usable_default=default_in_choices,
|
|
377
|
+
usable_default=default if default_in_choices else None,
|
|
378
|
+
ask_fn=ask_fn,
|
|
379
|
+
)
|
|
314
380
|
|
|
315
381
|
|
|
316
382
|
class KeyInput:
|
|
@@ -365,9 +431,7 @@ class PromptMatch:
|
|
|
365
431
|
return False
|
|
366
432
|
if (substr := self.substring) and substr in prompt_text:
|
|
367
433
|
return True
|
|
368
|
-
|
|
369
|
-
return True
|
|
370
|
-
return False
|
|
434
|
+
return bool((exact := self.exact) and exact == prompt_text)
|
|
371
435
|
|
|
372
436
|
def __eq__(self, value: object) -> bool:
|
|
373
437
|
if not isinstance(value, PromptMatch):
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from enum import StrEnum
|
|
5
|
+
from typing import Annotated, Generic, Literal, TypeVar
|
|
6
|
+
|
|
7
|
+
from model_lib import Entity
|
|
8
|
+
from pydantic import Field
|
|
9
|
+
from questionary import Choice
|
|
10
|
+
|
|
11
|
+
_UNDECIDED = "undecided"
|
|
12
|
+
|
|
13
|
+
T = TypeVar("T")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class PromptKind(StrEnum):
|
|
17
|
+
CONFIRM = "confirm"
|
|
18
|
+
TEXT = "text"
|
|
19
|
+
SELECT = "select"
|
|
20
|
+
SELECT_MULTIPLE = "select_multiple"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class MultiSelectStatus(StrEnum):
|
|
24
|
+
UNDECIDED = "undecided"
|
|
25
|
+
ANSWERED = "answered"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class SelectAlternative(Entity):
|
|
29
|
+
name: str
|
|
30
|
+
description: str | None = None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ConfirmQuestion(Entity):
|
|
34
|
+
kind: Literal[PromptKind.CONFIRM] = PromptKind.CONFIRM
|
|
35
|
+
prompt: str
|
|
36
|
+
response: Literal["undecided"] | bool = _UNDECIDED
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class TextQuestion(Entity):
|
|
40
|
+
kind: Literal[PromptKind.TEXT] = PromptKind.TEXT
|
|
41
|
+
prompt: str
|
|
42
|
+
response: str = _UNDECIDED
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class SelectQuestion(Entity):
|
|
46
|
+
kind: Literal[PromptKind.SELECT] = PromptKind.SELECT
|
|
47
|
+
prompt: str
|
|
48
|
+
alternatives: list[SelectAlternative] = Field(default_factory=list)
|
|
49
|
+
chosen: str | None = None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class SelectMultipleQuestion(Entity):
|
|
53
|
+
kind: Literal[PromptKind.SELECT_MULTIPLE] = PromptKind.SELECT_MULTIPLE
|
|
54
|
+
prompt: str
|
|
55
|
+
status: MultiSelectStatus = MultiSelectStatus.UNDECIDED
|
|
56
|
+
alternatives: list[SelectAlternative] = Field(default_factory=list)
|
|
57
|
+
checked: list[str] = Field(default_factory=list)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
PromptQuestion = Annotated[
|
|
61
|
+
ConfirmQuestion | TextQuestion | SelectQuestion | SelectMultipleQuestion,
|
|
62
|
+
Field(discriminator="kind"),
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class NonInteractivePromptFile(Entity):
|
|
67
|
+
questions: list[PromptQuestion] = Field(default_factory=list)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass
|
|
71
|
+
class ChoiceTyped(Generic[T]):
|
|
72
|
+
name: str
|
|
73
|
+
value: T
|
|
74
|
+
description: str | None = None
|
|
75
|
+
checked: bool = False
|
|
76
|
+
|
|
77
|
+
@classmethod
|
|
78
|
+
def from_descriptions(cls, descriptions: dict[str, str]) -> list[ChoiceTyped[str]]:
|
|
79
|
+
return [cls(name=name, value=name, description=description) for name, description in descriptions.items()] # type: ignore
|
|
80
|
+
|
|
81
|
+
def as_choice(self) -> Choice:
|
|
82
|
+
return Choice(
|
|
83
|
+
title=self.name,
|
|
84
|
+
value=self.value,
|
|
85
|
+
description=self.description,
|
|
86
|
+
checked=self.checked,
|
|
87
|
+
)
|
|
@@ -342,9 +342,8 @@ class ShellRun:
|
|
|
342
342
|
return False
|
|
343
343
|
|
|
344
344
|
def remove_callback():
|
|
345
|
-
with self._lock:
|
|
346
|
-
|
|
347
|
-
self.config.message_callbacks.remove(only_on_output_callback)
|
|
345
|
+
with self._lock, suppress(ValueError): # if the callback was already removed
|
|
346
|
+
self.config.message_callbacks.remove(only_on_output_callback)
|
|
348
347
|
|
|
349
348
|
with self._lock:
|
|
350
349
|
self.config.message_callbacks.append(only_on_output_callback)
|
|
@@ -362,7 +361,7 @@ class ShellRun:
|
|
|
362
361
|
try:
|
|
363
362
|
if callback(message):
|
|
364
363
|
self.config.message_callbacks.remove(callback)
|
|
365
|
-
except
|
|
364
|
+
except (TypeError, ValueError, RuntimeError, OSError, AttributeError) as e:
|
|
366
365
|
logger.error(f"Error in message callback: {e!r}")
|
|
367
366
|
|
|
368
367
|
def _dump_html_logs(self):
|
|
@@ -410,7 +409,7 @@ class ShellRun:
|
|
|
410
409
|
if no_raise:
|
|
411
410
|
try:
|
|
412
411
|
return self._complete_flag.result(timeout)
|
|
413
|
-
except Exception:
|
|
412
|
+
except Exception: # noqa: BLE001
|
|
414
413
|
return self
|
|
415
414
|
return self._complete_flag.result(timeout)
|
|
416
415
|
|