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.
Files changed (28) hide show
  1. {ask_shell-0.3.2 → ask_shell-0.5.0}/PKG-INFO +1 -1
  2. {ask_shell-0.3.2 → ask_shell-0.5.0}/ask_shell/__init__.py +1 -1
  3. {ask_shell-0.3.2 → ask_shell-0.5.0}/ask_shell/_internal/_run.py +36 -14
  4. {ask_shell-0.3.2 → ask_shell-0.5.0}/ask_shell/_internal/interactive.py +1 -0
  5. {ask_shell-0.3.2 → ask_shell-0.5.0}/ask_shell/_internal/models.py +4 -0
  6. {ask_shell-0.3.2 → ask_shell-0.5.0}/ask_shell/shell.py +2 -0
  7. ask_shell-0.5.0/ask_shell/test_docs.py +20 -0
  8. {ask_shell-0.3.2 → ask_shell-0.5.0}/pyproject.toml +36 -19
  9. {ask_shell-0.3.2 → ask_shell-0.5.0}/.gitignore +0 -0
  10. {ask_shell-0.3.2 → ask_shell-0.5.0}/LICENSE +0 -0
  11. {ask_shell-0.3.2 → ask_shell-0.5.0}/README.md +0 -0
  12. {ask_shell-0.3.2 → ask_shell-0.5.0}/ask_shell/__main__.py +0 -0
  13. {ask_shell-0.3.2 → ask_shell-0.5.0}/ask_shell/_internal/__init__.py +0 -0
  14. {ask_shell-0.3.2 → ask_shell-0.5.0}/ask_shell/_internal/_run_env.py +0 -0
  15. {ask_shell-0.3.2 → ask_shell-0.5.0}/ask_shell/_internal/events.py +0 -0
  16. {ask_shell-0.3.2 → ask_shell-0.5.0}/ask_shell/_internal/global_callbacks.py +0 -0
  17. {ask_shell-0.3.2 → ask_shell-0.5.0}/ask_shell/_internal/rich_live.py +0 -0
  18. {ask_shell-0.3.2 → ask_shell-0.5.0}/ask_shell/_internal/rich_live_callback.py +0 -0
  19. {ask_shell-0.3.2 → ask_shell-0.5.0}/ask_shell/_internal/rich_progress.py +0 -0
  20. {ask_shell-0.3.2 → ask_shell-0.5.0}/ask_shell/_internal/rich_run_state.py +0 -0
  21. {ask_shell-0.3.2 → ask_shell-0.5.0}/ask_shell/_internal/run_pool.py +0 -0
  22. {ask_shell-0.3.2 → ask_shell-0.5.0}/ask_shell/_internal/typer_command.py +0 -0
  23. {ask_shell-0.3.2 → ask_shell-0.5.0}/ask_shell/ask.py +0 -0
  24. {ask_shell-0.3.2 → ask_shell-0.5.0}/ask_shell/console.py +0 -0
  25. {ask_shell-0.3.2 → ask_shell-0.5.0}/ask_shell/py.typed +0 -0
  26. {ask_shell-0.3.2 → ask_shell-0.5.0}/ask_shell/settings.py +0 -0
  27. {ask_shell-0.3.2 → ask_shell-0.5.0}/ask_shell/shell_events.py +0 -0
  28. {ask_shell-0.3.2 → ask_shell-0.5.0}/scripts/fix_source_links.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ask-shell
3
- Version: 0.3.2
3
+ Version: 0.5.0
4
4
  Summary: CLIs with prompts, shell runs, and testable flows
5
5
  Author-email: EspenAlbert <espen.albert1@gmail.com>
6
6
  License-Expression: MIT
@@ -6,7 +6,7 @@ from ask_shell import shell_events
6
6
  from ask_shell import ask
7
7
  from ask_shell.settings import AskShellSettings
8
8
 
9
- VERSION = "0.3.2"
9
+ VERSION = "0.5.0"
10
10
  __all__ = [
11
11
  "shell",
12
12
  "console",
@@ -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: BaseException | None = None
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.3.2"
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
- "Development Status :: 4 - Beta",
14
- "Programming Language :: Python :: 3.13",
15
- "Programming Language :: Python :: 3.14",
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
- "ruff>=0.14",
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