ask-shell 0.3.2__tar.gz → 0.5.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.3.2 → ask_shell-0.5.0}/PKG-INFO +1 -1
- {ask_shell-0.3.2 → ask_shell-0.5.0}/ask_shell/__init__.py +1 -1
- {ask_shell-0.3.2 → ask_shell-0.5.0}/ask_shell/_internal/_run.py +36 -14
- {ask_shell-0.3.2 → ask_shell-0.5.0}/ask_shell/_internal/interactive.py +1 -0
- {ask_shell-0.3.2 → ask_shell-0.5.0}/ask_shell/_internal/models.py +4 -0
- {ask_shell-0.3.2 → ask_shell-0.5.0}/ask_shell/shell.py +2 -0
- ask_shell-0.5.0/ask_shell/test_docs.py +20 -0
- {ask_shell-0.3.2 → ask_shell-0.5.0}/pyproject.toml +36 -19
- {ask_shell-0.3.2 → ask_shell-0.5.0}/.gitignore +0 -0
- {ask_shell-0.3.2 → ask_shell-0.5.0}/LICENSE +0 -0
- {ask_shell-0.3.2 → ask_shell-0.5.0}/README.md +0 -0
- {ask_shell-0.3.2 → ask_shell-0.5.0}/ask_shell/__main__.py +0 -0
- {ask_shell-0.3.2 → ask_shell-0.5.0}/ask_shell/_internal/__init__.py +0 -0
- {ask_shell-0.3.2 → ask_shell-0.5.0}/ask_shell/_internal/_run_env.py +0 -0
- {ask_shell-0.3.2 → ask_shell-0.5.0}/ask_shell/_internal/events.py +0 -0
- {ask_shell-0.3.2 → ask_shell-0.5.0}/ask_shell/_internal/global_callbacks.py +0 -0
- {ask_shell-0.3.2 → ask_shell-0.5.0}/ask_shell/_internal/rich_live.py +0 -0
- {ask_shell-0.3.2 → ask_shell-0.5.0}/ask_shell/_internal/rich_live_callback.py +0 -0
- {ask_shell-0.3.2 → ask_shell-0.5.0}/ask_shell/_internal/rich_progress.py +0 -0
- {ask_shell-0.3.2 → ask_shell-0.5.0}/ask_shell/_internal/rich_run_state.py +0 -0
- {ask_shell-0.3.2 → ask_shell-0.5.0}/ask_shell/_internal/run_pool.py +0 -0
- {ask_shell-0.3.2 → ask_shell-0.5.0}/ask_shell/_internal/typer_command.py +0 -0
- {ask_shell-0.3.2 → ask_shell-0.5.0}/ask_shell/ask.py +0 -0
- {ask_shell-0.3.2 → ask_shell-0.5.0}/ask_shell/console.py +0 -0
- {ask_shell-0.3.2 → ask_shell-0.5.0}/ask_shell/py.typed +0 -0
- {ask_shell-0.3.2 → ask_shell-0.5.0}/ask_shell/settings.py +0 -0
- {ask_shell-0.3.2 → ask_shell-0.5.0}/ask_shell/shell_events.py +0 -0
- {ask_shell-0.3.2 → ask_shell-0.5.0}/scripts/fix_source_links.py +0 -0
|
@@ -40,6 +40,7 @@ from ask_shell._internal.events import (
|
|
|
40
40
|
ShellRunStdStarted,
|
|
41
41
|
)
|
|
42
42
|
from ask_shell._internal.models import (
|
|
43
|
+
AbortRetryError,
|
|
43
44
|
RunIncompleteError,
|
|
44
45
|
ShellConfig,
|
|
45
46
|
ShellRun,
|
|
@@ -370,6 +371,40 @@ def _attempt_run(
|
|
|
370
371
|
_runs.pop(key, None)
|
|
371
372
|
|
|
372
373
|
|
|
374
|
+
def _eval_should_retry(config: ShellConfig, result: ShellRun) -> bool:
|
|
375
|
+
"""Evaluate should_retry, raising AbortRetryError through if the callback raises it."""
|
|
376
|
+
try:
|
|
377
|
+
return config.should_retry(result)
|
|
378
|
+
except AbortRetryError:
|
|
379
|
+
raise
|
|
380
|
+
except Exception as e:
|
|
381
|
+
logger.warning(f"should_retry callback failed: {e!r}")
|
|
382
|
+
return False
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def _run_attempts(shell_run: ShellRun, output_dir: Path) -> BaseException | None:
|
|
386
|
+
config = shell_run.config
|
|
387
|
+
queue = shell_run._queue
|
|
388
|
+
for attempt in range(1, config.attempts + 1):
|
|
389
|
+
if attempt > 1:
|
|
390
|
+
queue.put_nowait(ShellRunRetryAttempt(attempt=attempt))
|
|
391
|
+
logger.warning(f"Retrying run {shell_run} attempt {attempt} of {config.attempts}")
|
|
392
|
+
attempt_log_prefix = config.run_log_stem(attempt)
|
|
393
|
+
result = _attempt_run(shell_run, output_dir, attempt_log_prefix)
|
|
394
|
+
match result:
|
|
395
|
+
case ShellRun() if result.clean_complete:
|
|
396
|
+
return None
|
|
397
|
+
case ShellRun():
|
|
398
|
+
try:
|
|
399
|
+
if not _eval_should_retry(config, result):
|
|
400
|
+
return None
|
|
401
|
+
except AbortRetryError as abort:
|
|
402
|
+
return abort
|
|
403
|
+
case BaseException():
|
|
404
|
+
return result
|
|
405
|
+
return None
|
|
406
|
+
|
|
407
|
+
|
|
373
408
|
def _execute_run(shell_run: ShellRun) -> ShellRun:
|
|
374
409
|
"""
|
|
375
410
|
Principles:
|
|
@@ -406,20 +441,7 @@ def _execute_run(shell_run: ShellRun) -> ShellRun:
|
|
|
406
441
|
with _run_completer(shell_run, consumer_future):
|
|
407
442
|
output_dir = config.run_output_dir_resolved()
|
|
408
443
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
409
|
-
error
|
|
410
|
-
for attempt in range(1, config.attempts + 1):
|
|
411
|
-
if attempt > 1:
|
|
412
|
-
queue.put_nowait(ShellRunRetryAttempt(attempt=attempt))
|
|
413
|
-
logger.warning(f"Retrying run {shell_run} attempt {attempt} of {config.attempts}")
|
|
414
|
-
attempt_log_prefix = config.run_log_stem(attempt)
|
|
415
|
-
|
|
416
|
-
result = _attempt_run(shell_run, output_dir, attempt_log_prefix)
|
|
417
|
-
match result:
|
|
418
|
-
case ShellRun() if result.clean_complete or not config.should_retry(result):
|
|
419
|
-
break
|
|
420
|
-
case BaseException():
|
|
421
|
-
error = result
|
|
422
|
-
break
|
|
444
|
+
error = _run_attempts(shell_run, output_dir)
|
|
423
445
|
queue.put_nowait(ShellRunAfter(run=shell_run, error=error))
|
|
424
446
|
shell_run._complete(error=error, queue_consumer=consumer_future)
|
|
425
447
|
return shell_run
|
|
@@ -472,6 +472,7 @@ class question_patcher(force_interactive):
|
|
|
472
472
|
q.application.renderer.output = output
|
|
473
473
|
|
|
474
474
|
def ask_question(self, q: Question, response_type: type[T]) -> T:
|
|
475
|
+
_ = response_type # binds generic T for return type inference
|
|
475
476
|
with create_pipe_input() as inp:
|
|
476
477
|
q.application.input = inp
|
|
477
478
|
input_response = self._next_response(q)
|
|
@@ -517,6 +517,10 @@ class RunIncompleteError(Exception):
|
|
|
517
517
|
self.killed = killed
|
|
518
518
|
|
|
519
519
|
|
|
520
|
+
class AbortRetryError(Exception):
|
|
521
|
+
"""Raise from should_retry to stop retrying with a custom error."""
|
|
522
|
+
|
|
523
|
+
|
|
520
524
|
class EmptyOutputError(Exception):
|
|
521
525
|
def __init__(self, run: ShellRun, stream: Literal["stdout", "stderr"] = "stdout"):
|
|
522
526
|
self.run = run
|
|
@@ -7,6 +7,7 @@ from ask_shell._internal._run import run_and_wait as _run_and_wait
|
|
|
7
7
|
from ask_shell._internal._run import run_error as _run_error
|
|
8
8
|
from ask_shell._internal._run import stop_runs_and_pool as _stop_runs_and_pool
|
|
9
9
|
from ask_shell._internal._run import wait_on_ok_errors as _wait_on_ok_errors
|
|
10
|
+
from ask_shell._internal.models import AbortRetryError as _AbortRetryError
|
|
10
11
|
from ask_shell._internal.models import ShellConfig as _ShellConfig
|
|
11
12
|
from ask_shell._internal.models import ShellError as _ShellError
|
|
12
13
|
from ask_shell._internal.models import ShellRun as _ShellRun
|
|
@@ -20,6 +21,7 @@ run_and_wait = _run_and_wait
|
|
|
20
21
|
run_error = _run_error
|
|
21
22
|
stop_runs_and_pool = _stop_runs_and_pool
|
|
22
23
|
wait_on_ok_errors = _wait_on_ok_errors
|
|
24
|
+
AbortRetryError = _AbortRetryError
|
|
23
25
|
ShellConfig = _ShellConfig
|
|
24
26
|
ShellError = _ShellError
|
|
25
27
|
ShellRun = _ShellRun
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from pytest_examples import CodeExample, EvalExample, find_examples
|
|
5
|
+
|
|
6
|
+
EXAMPLES_DIR = Path(__file__).parent.parent / "docs" / "examples"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@pytest.mark.parametrize("example", find_examples(EXAMPLES_DIR), ids=str)
|
|
10
|
+
def test_examples(example: CodeExample, eval_example: EvalExample):
|
|
11
|
+
eval_example.set_config(line_length=120, target_version="py313", ruff_ignore=["T"]) # pyright: ignore[reportArgumentType]
|
|
12
|
+
prefix = example.prefix_settings()
|
|
13
|
+
if prefix.get("test", "").startswith("skip"):
|
|
14
|
+
pytest.skip(prefix["test"])
|
|
15
|
+
if eval_example.update_examples:
|
|
16
|
+
eval_example.format(example)
|
|
17
|
+
eval_example.run_print_update(example)
|
|
18
|
+
else:
|
|
19
|
+
eval_example.lint(example)
|
|
20
|
+
eval_example.run_print_check(example)
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[project]
|
|
4
4
|
name = "ask-shell"
|
|
5
|
-
version = "0.
|
|
5
|
+
version = "0.5.0"
|
|
6
6
|
description = "CLIs with prompts, shell runs, and testable flows"
|
|
7
7
|
requires-python = ">=3.13"
|
|
8
8
|
license = "MIT"
|
|
@@ -10,9 +10,9 @@ license-files = ["LICENSE"]
|
|
|
10
10
|
readme = "README.md"
|
|
11
11
|
authors = [{ name = "EspenAlbert", email = "espen.albert1@gmail.com" }]
|
|
12
12
|
classifiers = [
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
"Development Status :: 4 - Beta",
|
|
14
|
+
"Programming Language :: Python :: 3.13",
|
|
15
|
+
"Programming Language :: Python :: 3.14",
|
|
16
16
|
]
|
|
17
17
|
dependencies = [
|
|
18
18
|
"model-lib[toml]",
|
|
@@ -28,12 +28,7 @@ build-backend = "hatchling.build"
|
|
|
28
28
|
|
|
29
29
|
[tool.hatch.build.targets.sdist]
|
|
30
30
|
include = ["*/**/*.py", "*/py.typed"]
|
|
31
|
-
exclude = [
|
|
32
|
-
"*_examples.py",
|
|
33
|
-
"*_test.py",
|
|
34
|
-
"conftest.py",
|
|
35
|
-
"test_*.json",
|
|
36
|
-
]
|
|
31
|
+
exclude = ["*_examples.py", "*_test.py", "conftest.py", "test_*.json"]
|
|
37
32
|
|
|
38
33
|
# === DO_NOT_EDIT: path-sync pkg-ext ===
|
|
39
34
|
[tool.pkg-ext]
|
|
@@ -41,6 +36,12 @@ tag_prefix = "v"
|
|
|
41
36
|
max_bump_type = "minor"
|
|
42
37
|
# === OK_EDIT: path-sync pkg-ext ===
|
|
43
38
|
|
|
39
|
+
[tool.pkg-ext.groups.shell]
|
|
40
|
+
examples_include = [
|
|
41
|
+
"AbortRetryError",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
|
|
44
45
|
# === DO_NOT_EDIT: path-sync ruff ===
|
|
45
46
|
[tool.ruff]
|
|
46
47
|
line-length = 120
|
|
@@ -55,12 +56,13 @@ target-version = "py313"
|
|
|
55
56
|
# UP046: use type parameters on functions instead of TypeVar
|
|
56
57
|
# UP047: use type parameters instead of `Generic[T]` subclass
|
|
57
58
|
extend-ignore = ["E501", "UP006", "UP007", "UP035", "UP040", "UP046", "UP047"]
|
|
58
|
-
extend-select = ["Q", "RUF100", "C90", "UP", "I", "T"]
|
|
59
|
+
extend-select = ["Q", "RUF100", "C90", "UP", "I", "T", "FURB"]
|
|
59
60
|
# === OK_EDIT: path-sync ruff ===
|
|
60
61
|
|
|
61
62
|
# === DO_NOT_EDIT: path-sync pytest ===
|
|
62
63
|
[tool.pytest.ini_options]
|
|
63
|
-
addopts = "-s -vv --log-cli-level=INFO"
|
|
64
|
+
addopts = "-s -vv --log-cli-level=INFO -m 'not manual'"
|
|
65
|
+
markers = ["manual: test requires manual env setup (not run in CI)"]
|
|
64
66
|
python_files = ["*_test.py", "test_*.py"]
|
|
65
67
|
asyncio_mode = "auto"
|
|
66
68
|
# === OK_EDIT: path-sync pytest ===
|
|
@@ -71,22 +73,37 @@ omit = ["*_test.py"]
|
|
|
71
73
|
exclude_lines = ["no cov", "if __name__ == .__main__.:"]
|
|
72
74
|
# === OK_EDIT: path-sync coverage ===
|
|
73
75
|
|
|
76
|
+
# === DO_NOT_EDIT: path-sync vulture ===
|
|
77
|
+
[tool.vulture]
|
|
78
|
+
min_confidence = 80
|
|
79
|
+
ignore_names = ["mock_*", "Mock*", "*_mock"]
|
|
80
|
+
exclude = [".venv/", "*test.py"]
|
|
81
|
+
# === OK_EDIT: path-sync vulture ===
|
|
82
|
+
|
|
83
|
+
# === DO_NOT_EDIT: path-sync dev-lint ===
|
|
74
84
|
[dependency-groups]
|
|
75
|
-
dev = [
|
|
85
|
+
dev-lint = [
|
|
76
86
|
"pyright>=1.1",
|
|
87
|
+
"ruff>=0.14",
|
|
88
|
+
"vulture>=2.14",
|
|
89
|
+
]
|
|
90
|
+
# === OK_EDIT: path-sync dev-lint ===
|
|
91
|
+
dev-test = [
|
|
77
92
|
"pytest>=9.0",
|
|
78
93
|
"pytest-asyncio>=0.24",
|
|
79
94
|
"pytest-cov>=7.0",
|
|
95
|
+
"pytest-examples>=0.0.18",
|
|
80
96
|
"pytest-model-lib",
|
|
81
|
-
"
|
|
97
|
+
"pytest-regressions>=2.6",
|
|
82
98
|
]
|
|
83
|
-
|
|
99
|
+
# === DO_NOT_EDIT: path-sync dev ===
|
|
100
|
+
dev = [{include-group = "dev-lint"}, {include-group = "dev-test"}]
|
|
101
|
+
# === OK_EDIT: path-sync dev ===
|
|
102
|
+
# === DO_NOT_EDIT: path-sync release ===
|
|
103
|
+
release = ["pkg-ext"]
|
|
104
|
+
# === OK_EDIT: path-sync release ===
|
|
84
105
|
# === DO_NOT_EDIT: path-sync docs ===
|
|
85
106
|
docs = [
|
|
86
107
|
"mkdocs-material>=9.5",
|
|
87
108
|
]
|
|
88
109
|
# === OK_EDIT: path-sync docs ===
|
|
89
|
-
|
|
90
|
-
# === DO_NOT_EDIT: path-sync release ===
|
|
91
|
-
release = ["pkg-ext"]
|
|
92
|
-
# === OK_EDIT: path-sync release ===
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|