cli-error 0.1.0__tar.gz → 0.1.1__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 (70) hide show
  1. {cli_error-0.1.0 → cli_error-0.1.1}/.sandbox/README.md +0 -4
  2. {cli_error-0.1.0 → cli_error-0.1.1}/.sandbox/docker-compose.yml +0 -1
  3. {cli_error-0.1.0 → cli_error-0.1.1}/.tasker/s01-extract-repo-skills-error-reporting/README.md +5 -5
  4. {cli_error-0.1.0 → cli_error-0.1.1}/.tasker/s01-extract-repo-skills-error-reporting/s01t15-reconsider-derive-debug-stderr-console.md +1 -1
  5. {cli_error-0.1.0 → cli_error-0.1.1}/.tasker/s01-extract-repo-skills-error-reporting/s01t16-optionally-show-locals-in-debug.md +1 -1
  6. {cli_error-0.1.0 → cli_error-0.1.1}/.tasker/s01-extract-repo-skills-error-reporting/s01t17-test-debug-traceback-includes-the.md +1 -1
  7. {cli_error-0.1.0 → cli_error-0.1.1}/.tasker/s01-extract-repo-skills-error-reporting/s01t19-consolidate-shared-test-helpers-across.md +1 -1
  8. {cli_error-0.1.0 → cli_error-0.1.1}/PKG-INFO +1 -1
  9. {cli_error-0.1.0 → cli_error-0.1.1}/README.md +24 -6
  10. {cli_error-0.1.0 → cli_error-0.1.1}/pyproject.toml +1 -1
  11. {cli_error-0.1.0 → cli_error-0.1.1}/src/cli_error/_console.py +11 -0
  12. {cli_error-0.1.0 → cli_error-0.1.1}/src/cli_error/_reporter.py +18 -6
  13. cli_error-0.1.1/tests/helpers.py +33 -0
  14. {cli_error-0.1.0 → cli_error-0.1.1}/tests/test_console.py +13 -39
  15. {cli_error-0.1.0 → cli_error-0.1.1}/tests/test_errors.py +2 -3
  16. {cli_error-0.1.0 → cli_error-0.1.1}/tests/test_render.py +4 -19
  17. {cli_error-0.1.0 → cli_error-0.1.1}/tests/test_reporter.py +175 -0
  18. {cli_error-0.1.0 → cli_error-0.1.1}/uv.lock +35 -35
  19. cli_error-0.1.0/.claude/scheduled_tasks.lock +0 -1
  20. cli_error-0.1.0/.tasker/.closed +0 -16
  21. cli_error-0.1.0/.tasker/.recent +0 -1
  22. {cli_error-0.1.0 → cli_error-0.1.1}/.flake8 +0 -0
  23. {cli_error-0.1.0 → cli_error-0.1.1}/.github/workflows/ci.yml +0 -0
  24. {cli_error-0.1.0 → cli_error-0.1.1}/.github/workflows/release.yml +0 -0
  25. {cli_error-0.1.0 → cli_error-0.1.1}/.gitignore +0 -0
  26. {cli_error-0.1.0 → cli_error-0.1.1}/.mcp.json +0 -0
  27. {cli_error-0.1.0 → cli_error-0.1.1}/.sandbox/.env.example +0 -0
  28. {cli_error-0.1.0 → cli_error-0.1.1}/.sandbox/base/Dockerfile +0 -0
  29. {cli_error-0.1.0 → cli_error-0.1.1}/.sandbox/base/tmux.conf +0 -0
  30. {cli_error-0.1.0 → cli_error-0.1.1}/.sandbox/base/zshrc +0 -0
  31. {cli_error-0.1.0 → cli_error-0.1.1}/.sandbox/run.sh +0 -0
  32. {cli_error-0.1.0 → cli_error-0.1.1}/.sandbox/scripts/_lib.sh +0 -0
  33. {cli_error-0.1.0 → cli_error-0.1.1}/.sandbox/scripts/attach.sh +0 -0
  34. {cli_error-0.1.0 → cli_error-0.1.1}/.sandbox/scripts/build.sh +0 -0
  35. {cli_error-0.1.0 → cli_error-0.1.1}/.sandbox/scripts/claude.sh +0 -0
  36. {cli_error-0.1.0 → cli_error-0.1.1}/.sandbox/scripts/shell.sh +0 -0
  37. {cli_error-0.1.0 → cli_error-0.1.1}/.sandbox/variants/python/Dockerfile +0 -0
  38. {cli_error-0.1.0 → cli_error-0.1.1}/.sandbox/variants/python/compose.override.yml +0 -0
  39. {cli_error-0.1.0 → cli_error-0.1.1}/.sandbox/worktree_run.sh +0 -0
  40. {cli_error-0.1.0 → cli_error-0.1.1}/.sandbox/worktree_shell.sh +0 -0
  41. {cli_error-0.1.0 → cli_error-0.1.1}/.tasker/.gitignore +0 -0
  42. {cli_error-0.1.0 → cli_error-0.1.1}/.tasker/archive/.gitkeep +0 -0
  43. {cli_error-0.1.0 → cli_error-0.1.1}/.tasker/s01-extract-repo-skills-error-reporting/s01t01-rename-package-structure-clierror-cliexit.md +0 -0
  44. {cli_error-0.1.0 → cli_error-0.1.1}/.tasker/s01-extract-repo-skills-error-reporting/s01t02-fluent-builder-for-clierror-context.md +0 -0
  45. {cli_error-0.1.0 → cli_error-0.1.1}/.tasker/s01-extract-repo-skills-error-reporting/s01t03-render-formatted-error.md +0 -0
  46. {cli_error-0.1.0 → cli_error-0.1.1}/.tasker/s01-extract-repo-skills-error-reporting/s01t04-error-handler-errorreporter-context-manager.md +0 -0
  47. {cli_error-0.1.0 → cli_error-0.1.1}/.tasker/s01-extract-repo-skills-error-reporting/s01t05-cause-chain-rendering.md +0 -0
  48. {cli_error-0.1.0 → cli_error-0.1.1}/.tasker/s01-extract-repo-skills-error-reporting/s01t06-debug-traceback.md +0 -0
  49. {cli_error-0.1.0 → cli_error-0.1.1}/.tasker/s01-extract-repo-skills-error-reporting/s01t07-console-creation-with-style-overrides.md +0 -0
  50. {cli_error-0.1.0 → cli_error-0.1.1}/.tasker/s01-extract-repo-skills-error-reporting/s01t08-library-documentation-readme.md +0 -0
  51. {cli_error-0.1.0 → cli_error-0.1.1}/.tasker/s01-extract-repo-skills-error-reporting/s01t09-deployment-setup-ci-release-workflows.md +0 -0
  52. {cli_error-0.1.0 → cli_error-0.1.1}/.tasker/s01-extract-repo-skills-error-reporting/s01t10-unify-markup-construction-template-args.md +0 -0
  53. {cli_error-0.1.0 → cli_error-0.1.1}/.tasker/s01-extract-repo-skills-error-reporting/s01t11-refine-detail-single-last-wins.md +0 -0
  54. {cli_error-0.1.0 → cli_error-0.1.1}/.tasker/s01-extract-repo-skills-error-reporting/s01t12-revisit-props-markup-first-prop.md +0 -0
  55. {cli_error-0.1.0 → cli_error-0.1.1}/.tasker/s01-extract-repo-skills-error-reporting/s01t13-store-detail-as-ready-to.md +0 -0
  56. {cli_error-0.1.0 → cli_error-0.1.1}/.tasker/s01-extract-repo-skills-error-reporting/s01t14-handle-empty-message-cause-in.md +0 -0
  57. {cli_error-0.1.0 → cli_error-0.1.1}/.tasker/s01-extract-repo-skills-error-reporting/s01t18-add-debug-gated-diagnostic-helpers.md +0 -0
  58. {cli_error-0.1.0 → cli_error-0.1.1}/.tasker/s01-extract-repo-skills-error-reporting/s01t20-add-a-safe-post-construction.md +0 -0
  59. {cli_error-0.1.0 → cli_error-0.1.1}/CLAUDE.md +0 -0
  60. {cli_error-0.1.0 → cli_error-0.1.1}/CONTEXT.md +0 -0
  61. {cli_error-0.1.0 → cli_error-0.1.1}/docs/adr/0001-error-formatting-and-theming.md +0 -0
  62. {cli_error-0.1.0 → cli_error-0.1.1}/docs/adr/0002-cli-reporter-output-facade.md +0 -0
  63. {cli_error-0.1.0 → cli_error-0.1.1}/docs/agents/dev-loop.md +0 -0
  64. {cli_error-0.1.0 → cli_error-0.1.1}/docs/agents/task-tracker.md +0 -0
  65. {cli_error-0.1.0 → cli_error-0.1.1}/src/cli_error/__init__.py +0 -0
  66. {cli_error-0.1.0 → cli_error-0.1.1}/src/cli_error/_errors.py +0 -0
  67. {cli_error-0.1.0 → cli_error-0.1.1}/src/cli_error/_render.py +0 -0
  68. {cli_error-0.1.0 → cli_error-0.1.1}/src/cli_error/py.typed +0 -0
  69. {cli_error-0.1.0 → cli_error-0.1.1}/tests/__init__.py +0 -0
  70. {cli_error-0.1.0 → cli_error-0.1.1}/tox.ini +0 -0
@@ -47,10 +47,6 @@ build when the images already exist.
47
47
  Claude credentials live in `~/.claude` / `~/.claude.json` on the host (bind-mounted
48
48
  into the container), so authentication persists across container rebuilds.
49
49
 
50
- The host's `~/repo-skills` is bind-mounted **read-only** at `~/repo-skills` inside
51
- the container as a reference source, so the sandbox can consult it without
52
- modifying it or leaking container-generated files back onto the host.
53
-
54
50
  ## Daily usage
55
51
 
56
52
  ```bash
@@ -19,7 +19,6 @@ services:
19
19
  - ./base/zshrc:/home/${USER}/.zshrc:ro
20
20
  - ~/.claude:/home/${USER}/.claude
21
21
  - ~/.claude.json:/home/${USER}/.claude.json
22
- - ~/repo-skills:/home/${USER}/repo-skills:ro
23
22
  working_dir: /work
24
23
  environment:
25
24
  - TERM=xterm-256color
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  id: s01
3
3
  slug: extract-repo-skills-error-reporting
4
- status: pending
4
+ status: done
5
5
  ---
6
6
 
7
7
  # Extract repo-skills error reporting into cli-error library
@@ -53,9 +53,9 @@ status: pending
53
53
  - [x] [s01t12](s01t12-revisit-props-markup-first-prop.md): Revisit props: markup-first `prop` with template+args, roles as thin helpers
54
54
  - [x] [s01t13](s01t13-store-detail-as-ready-to.md): Store detail as ready-to-print [misc] markup (restore render-seam invariant)
55
55
  - [x] [s01t14](s01t14-handle-empty-message-cause-in.md): Handle empty-message cause in cause-chain rendering
56
- - [ ] [s01t15](s01t15-reconsider-derive-debug-stderr-console.md): Debug stderr console inherits user intent (no_color + theme), not per-stream geometry
57
- - [ ] [s01t16](s01t16-optionally-show-locals-in-debug.md): Add opt-in show_locals to the debug traceback
58
- - [ ] [s01t17](s01t17-test-debug-traceback-includes-the.md): Test: debug traceback includes the exception cause chain
56
+ - [x] [s01t15](s01t15-reconsider-derive-debug-stderr-console.md): Debug stderr console inherits user intent (no_color + theme), not per-stream geometry
57
+ - [x] [s01t16](s01t16-optionally-show-locals-in-debug.md): Add opt-in show_locals to the debug traceback
58
+ - [x] [s01t17](s01t17-test-debug-traceback-includes-the.md): Test: debug traceback includes the exception cause chain
59
59
  - [x] [s01t18](s01t18-add-debug-gated-diagnostic-helpers.md): Rename ErrorReporter to CliReporter and grow it into the CLI output façade
60
- - [ ] [s01t19](s01t19-consolidate-shared-test-helpers-across.md): Consolidate shared test helpers into tests/_helpers.py
60
+ - [x] [s01t19](s01t19-consolidate-shared-test-helpers-across.md): Consolidate shared test helpers into tests/_helpers.py
61
61
  - [x] [s01t20](s01t20-add-a-safe-post-construction.md): Add a safe post-construction debug toggle to CliReporter
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  id: s01t15
3
3
  slug: reconsider-derive-debug-stderr-console
4
- status: pending
4
+ status: done
5
5
  ---
6
6
 
7
7
  # Debug stderr console inherits user intent (no_color + theme), not per-stream geometry
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  id: s01t16
3
3
  slug: optionally-show-locals-in-debug
4
- status: pending
4
+ status: done
5
5
  ---
6
6
 
7
7
  # Add opt-in show_locals to the debug traceback
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  id: s01t17
3
3
  slug: test-debug-traceback-includes-the
4
- status: pending
4
+ status: done
5
5
  ---
6
6
 
7
7
  # Test: debug traceback includes the exception cause chain
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  id: s01t19
3
3
  slug: consolidate-shared-test-helpers-across
4
- status: pending
4
+ status: done
5
5
  ---
6
6
 
7
7
  # Consolidate shared test helpers into tests/_helpers.py
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cli-error
3
- Version: 0.1.0
3
+ Version: 0.1.1
4
4
  Summary: Error handling and reporting utilities for CLI applications.
5
5
  Requires-Python: >=3.12
6
6
  Requires-Dist: rich
@@ -198,10 +198,12 @@ if __name__ == "__main__":
198
198
  main()
199
199
  ```
200
200
 
201
- `CliReporter(console, *, debug=False, console_err=None)` — when `console_err` is
202
- omitted a stderr console is built for you. Wire `--debug` at construction as
203
- above; the flag is also a plain public attribute you can toggle later
204
- (`reporter.debug = True`).
201
+ `CliReporter(console, *, debug=False, show_locals=False, console_err=None)` — when
202
+ `console_err` is omitted a stderr console is built for you, inheriting the passed
203
+ console's `no_color` and theme so debug output matches your main console. Wire
204
+ `--debug` at construction as above; the flag is also a plain public attribute you
205
+ can toggle later (`reporter.debug = True`). `show_locals` sets the default for
206
+ `debug_traceback` (see below).
205
207
 
206
208
  ### Cause chains
207
209
 
@@ -233,7 +235,7 @@ Because they self-gate, you can sprinkle them freely without guarding each call.
233
235
  | Method | Emits (only when `debug`) |
234
236
  | ------------------------------ | -------------------------------------------------- |
235
237
  | `debug_print(template, **args)`| a trusted template with escaped args |
236
- | `debug_traceback()` | the currently-handled exception traceback |
238
+ | `debug_traceback(show_locals=None)` | the currently-handled exception traceback |
237
239
  | `debug_cmd(cmd, cwd=None)` | a subprocess command line and optional working dir |
238
240
  | `debug_output(stdout, stderr)` | captured subprocess output (empty streams skipped) |
239
241
 
@@ -250,7 +252,10 @@ reporter.debug_output(result.stdout, result.stderr)
250
252
 
251
253
  `debug_traceback()` is what `handler()` calls for you on an uncaught exception,
252
254
  so a `--debug` run automatically shows the full traceback on stderr alongside
253
- the rendered error.
255
+ the rendered error. Pass `show_locals=True` (per call, or as the
256
+ `CliReporter(show_locals=True)` default) to include each frame's local variables
257
+ — useful when debugging, but keep it developer-only since locals can expose
258
+ secrets like tokens or passwords.
254
259
 
255
260
  ## Customizing the theme
256
261
 
@@ -285,3 +290,16 @@ The repository is checked with `tox` (typecheck, test, lint):
285
290
  ```bash
286
291
  uv run tox
287
292
  ```
293
+
294
+ ## Release Notes
295
+
296
+ ### v0.1.1
297
+ - `debug_traceback` can now render per-frame locals — opt in with `CliReporter(show_locals=True)` or a per-call `debug_traceback(show_locals=True)` override (locals may expose secrets, so keep it developer-only).
298
+ - The auto-built stderr console now inherits the injected console's `no_color` and theme intent, so debug output matches your main console's styling.
299
+
300
+ ### v0.1.0
301
+ - `CliError` — structured exceptions with Rich-markup messages and a fluent builder for hints, detail blocks, and labelled context properties.
302
+ - `CliReporter` — output/exit façade: a context manager that maps exceptions to exit codes, renders the `caused by:` cause chain, and gates debug diagnostics behind a `debug` flag.
303
+ - `CliExit` — a clean-exit signal that prints its message and exits with status 0.
304
+ - Theming via `make_console`, `make_theme`, and `DEFAULT_STYLES` — Rich consoles with default style roles, per-role overrides, `no_color` degradation, and stderr routing.
305
+ - Automatic escaping of untrusted template/property values, plus standalone `render_error`, `render_template`, and `print_error` helpers.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "cli-error"
3
- version = "0.1.0"
3
+ version = "0.1.1"
4
4
  description = "Error handling and reporting utilities for CLI applications."
5
5
  requires-python = ">=3.12"
6
6
  dependencies = ["rich"]
@@ -38,3 +38,14 @@ def make_console(
38
38
  stderr=stderr,
39
39
  no_color=no_color,
40
40
  )
41
+
42
+
43
+ def derive_stderr_console(console: Console) -> Console:
44
+ """Build a stderr console that inherits ``console``'s user intent."""
45
+ return make_console(
46
+ stderr=True,
47
+ no_color=console.no_color,
48
+ styles={
49
+ role: console.get_style(role, default=Style()) for role in DEFAULT_STYLES
50
+ },
51
+ )
@@ -7,7 +7,7 @@ from typing import Any
7
7
 
8
8
  from rich.console import Console
9
9
 
10
- from ._console import make_console
10
+ from ._console import derive_stderr_console
11
11
  from ._errors import CliExit, print_error
12
12
  from ._render import render_template
13
13
 
@@ -15,18 +15,18 @@ from ._render import render_template
15
15
  class CliReporter:
16
16
  """Single integration point for a CLI's terminal output."""
17
17
 
18
- debug: bool = False
19
-
20
18
  def __init__(
21
19
  self,
22
20
  console: Console,
23
21
  *,
24
22
  debug: bool = False,
23
+ show_locals: bool = False,
25
24
  console_err: Console | None = None,
26
25
  ) -> None:
27
26
  self.debug = debug
27
+ self.show_locals = show_locals
28
28
  self.console = console
29
- self.console_err = console_err or make_console(stderr=True)
29
+ self.console_err = console_err or derive_stderr_console(console)
30
30
 
31
31
  def print(self, template: str, /, *, end: str = "\n", **args: Any) -> None:
32
32
  """Render a trusted template with escaped args to the stdout console."""
@@ -41,11 +41,19 @@ class CliReporter:
41
41
  if self.debug:
42
42
  _emit(self.console_err, template, end, **args)
43
43
 
44
- def debug_traceback(self) -> None:
44
+ def debug_traceback(self, *, show_locals: bool | None = None) -> None:
45
45
  """Emit the currently-handled traceback to the stderr console.
46
46
 
47
47
  No-op unless ``debug`` is set. Safely no-ops outside an ``except``
48
48
  block: ``print_exception`` reads ``sys.exc_info()``.
49
+
50
+ ``show_locals`` overrides the instance default per call; when left as
51
+ ``None`` the traceback defers to the ``show_locals`` flag the reporter
52
+ was constructed with.
53
+
54
+ Caution: ``show_locals`` renders every frame's local variables verbatim
55
+ to stderr, which may expose secrets (tokens, passwords). Keep it gated
56
+ behind an explicit developer-only flag.
49
57
  """
50
58
  if not self.debug:
51
59
  return
@@ -53,7 +61,11 @@ class CliReporter:
53
61
  if sys.exc_info()[0] is None:
54
62
  return
55
63
 
56
- self.console_err.print_exception()
64
+ effective = show_locals
65
+ if effective is None:
66
+ effective = self.show_locals
67
+
68
+ self.console_err.print_exception(show_locals=effective)
57
69
 
58
70
  def debug_cmd(self, cmd: list[str], cwd: Path | None = None) -> None:
59
71
  """Emit a subprocess command (and optional cwd) as a debug diagnostic."""
@@ -0,0 +1,33 @@
1
+ from collections.abc import Callable
2
+
3
+ from rich.console import Console
4
+ from rich.theme import Theme
5
+
6
+ from cli_error import DEFAULT_STYLES
7
+
8
+ _ROLES = tuple(DEFAULT_STYLES)
9
+
10
+
11
+ def forced_terminal(theme: Theme, *, no_color: bool = False) -> Console:
12
+ return Console(
13
+ theme=theme,
14
+ force_terminal=True,
15
+ color_system="standard",
16
+ no_color=no_color,
17
+ highlight=False,
18
+ width=80,
19
+ )
20
+
21
+
22
+ def theme_from(console: Console, roles: tuple[str, ...] = _ROLES) -> Theme:
23
+ return Theme({role: console.get_style(role) for role in roles})
24
+
25
+
26
+ def capture_emit(console: Console, emit: Callable[[Console], None]) -> str:
27
+ with console.capture() as cap:
28
+ emit(console)
29
+ return cap.get()
30
+
31
+
32
+ def capture(console: Console, markup: str) -> str:
33
+ return capture_emit(console, lambda c: c.print(markup))
@@ -1,33 +1,7 @@
1
- from rich.console import Console
2
1
  from rich.style import Style
3
- from rich.theme import Theme
4
2
 
5
3
  from cli_error import DEFAULT_STYLES, make_console, make_theme
6
-
7
- _ROLES = ("id", "data", "path", "cmd", "misc", "warn", "err")
8
-
9
-
10
- def _forced(theme: Theme, *, no_color: bool = False) -> Console:
11
- """A forced-ANSI console carrying ``theme``; ``no_color`` strips escapes."""
12
- return Console(
13
- theme=theme,
14
- force_terminal=True,
15
- color_system="standard",
16
- no_color=no_color,
17
- highlight=False,
18
- width=80,
19
- )
20
-
21
-
22
- def _terminal(theme_source: Console) -> Console:
23
- """A forced-ANSI console carrying ``theme_source``'s resolved roles."""
24
- return _forced(Theme({role: theme_source.get_style(role) for role in _ROLES}))
25
-
26
-
27
- def _capture(console: Console, markup: str) -> str:
28
- with console.capture() as capture:
29
- console.print(markup)
30
- return capture.get()
4
+ from tests.helpers import _ROLES, capture, forced_terminal, theme_from
31
5
 
32
6
 
33
7
  def test_default_console_maps_roles_to_canonical_styles() -> None:
@@ -37,8 +11,8 @@ def test_default_console_maps_roles_to_canonical_styles() -> None:
37
11
 
38
12
 
39
13
  def test_default_roles_render_as_distinct_ansi_runs() -> None:
40
- console = _terminal(make_console())
41
- out = _capture(console, "[id]a[/id][path]b[/path][err]c[/err]")
14
+ console = forced_terminal(theme_from(make_console()))
15
+ out = capture(console, "[id]a[/id][path]b[/path][err]c[/err]")
42
16
 
43
17
  id_run = console.get_style("id").render("a")
44
18
  path_run = console.get_style("path").render("b")
@@ -79,18 +53,18 @@ def test_unknown_role_key_is_added_and_never_raises() -> None:
79
53
  console = make_console(styles={"custom": "magenta"})
80
54
  assert console.get_style("custom") == Style.parse("magenta")
81
55
  # An undefined role degrades to plain text on a colour terminal, no error.
82
- terminal = _terminal(make_console())
83
- assert _capture(terminal, "[undefined]x[/undefined]") == "x\n"
56
+ terminal = forced_terminal(theme_from(make_console()))
57
+ assert capture(terminal, "[undefined]x[/undefined]") == "x\n"
84
58
 
85
59
 
86
60
  def test_no_color_suppresses_ansi_on_a_forced_terminal() -> None:
87
61
  # Positive control: an identical forced terminal DOES emit ANSI, proving the
88
62
  # no_color flag below is load-bearing rather than vacuously satisfied.
89
- coloured = _forced(make_theme(), no_color=False)
90
- plain = _forced(make_theme(), no_color=True)
63
+ coloured = forced_terminal(make_theme(), no_color=False)
64
+ plain = forced_terminal(make_theme(), no_color=True)
91
65
 
92
- coloured_out = _capture(coloured, "[id]x[/id]")
93
- plain_out = _capture(plain, "[id]x[/id]")
66
+ coloured_out = capture(coloured, "[id]x[/id]")
67
+ plain_out = capture(plain, "[id]x[/id]")
94
68
 
95
69
  assert "\x1b" in coloured_out
96
70
  assert "\x1b" not in plain_out
@@ -99,7 +73,7 @@ def test_no_color_suppresses_ansi_on_a_forced_terminal() -> None:
99
73
 
100
74
  def test_no_color_console_exposes_flag_and_renders_plain_text() -> None:
101
75
  console = make_console(no_color=True)
102
- out = _capture(console, "[id]x[/id]")
76
+ out = capture(console, "[id]x[/id]")
103
77
 
104
78
  assert out == "x\n"
105
79
  assert "\x1b" not in out
@@ -125,8 +99,8 @@ def test_default_styles_is_public_and_has_the_seven_roles() -> None:
125
99
 
126
100
 
127
101
  def test_make_theme_is_attachable_to_a_plain_console() -> None:
128
- console = _forced(make_theme())
129
- out = _capture(console, "[id]x[/id]")
102
+ console = forced_terminal(make_theme())
103
+ out = capture(console, "[id]x[/id]")
130
104
  assert console.get_style("id").render("x") in out
131
105
 
132
106
 
@@ -141,4 +115,4 @@ def test_override_value_may_be_a_prebuilt_style_object() -> None:
141
115
  console = make_console(styles={"id": override})
142
116
 
143
117
  assert console.get_style("id") == override
144
- assert _capture(console, "[id]x[/id]") == "x\n"
118
+ assert capture(console, "[id]x[/id]") == "x\n"
@@ -2,6 +2,7 @@ import pytest
2
2
  from rich.console import Console
3
3
 
4
4
  from cli_error import CliError, CliExit, escape, make_console
5
+ from tests.helpers import capture
5
6
 
6
7
  PROP_METHODS = ["prop_id", "prop_path", "prop_data", "prop_cmd", "prop_misc", "prop"]
7
8
  PROP_HELPERS = {
@@ -90,9 +91,7 @@ def test_make_console_returns_console() -> None:
90
91
  console = make_console()
91
92
  assert isinstance(console, Console)
92
93
  assert console.stderr is False
93
- with console.capture() as capture:
94
- console.print("[err]x[/err]")
95
- assert "x" in capture.get()
94
+ assert "x" in capture(console, "[err]x[/err]")
96
95
 
97
96
 
98
97
  def test_make_console_stderr_variant_sets_flag_and_is_themed() -> None:
@@ -1,14 +1,10 @@
1
- from rich.console import Console
2
- from rich.theme import Theme
3
-
4
1
  from cli_error import CliError, make_console, print_error, render_error
2
+ from tests.helpers import capture_emit, forced_terminal, theme_from
5
3
 
6
4
 
7
5
  def _render(ex: Exception) -> str:
8
6
  console = make_console()
9
- with console.capture() as capture:
10
- print_error(ex, console)
11
- return capture.get()
7
+ return capture_emit(console, lambda c: print_error(ex, c))
12
8
 
13
9
 
14
10
  def test_full_layout_renders_in_documented_order() -> None:
@@ -73,20 +69,9 @@ def test_no_props_detail_or_hint_emits_only_message() -> None:
73
69
 
74
70
  def test_role_markup_is_applied_to_values_on_a_terminal() -> None:
75
71
  palette = make_console()
76
- theme = Theme(
77
- {role: palette.get_style(role) for role in ("id", "path", "misc", "err")}
78
- )
79
- console = Console(
80
- theme=theme,
81
- force_terminal=True,
82
- color_system="standard",
83
- highlight=False,
84
- width=80,
85
- )
72
+ console = forced_terminal(theme_from(palette, roles=("id", "path", "misc", "err")))
86
73
  error = CliError("m").prop_id("who", "abc").prop_path("where", "/p").detail("dee")
87
- with console.capture() as capture:
88
- print_error(error, console)
89
- out = capture.get()
74
+ out = capture_emit(console, lambda c: print_error(error, c))
90
75
 
91
76
  id_run = console.get_style("id").render("abc")
92
77
  path_run = console.get_style("path").render("/p")
@@ -2,6 +2,7 @@ from pathlib import Path
2
2
 
3
3
  import pytest
4
4
  from rich.console import Console
5
+ from rich.style import Style
5
6
 
6
7
  import cli_error
7
8
  from cli_error import CliError, CliExit, CliReporter, make_console, make_theme
@@ -17,6 +18,64 @@ def make_color_console() -> Console:
17
18
  )
18
19
 
19
20
 
21
+ def derive_console_err(console: Console) -> Console:
22
+ """Auto-derived stderr console for a debug reporter wired to ``console``."""
23
+ return CliReporter(console, debug=True).console_err
24
+
25
+
26
+ class _Inner(Exception):
27
+ pass
28
+
29
+
30
+ class _Outer(Exception):
31
+ pass
32
+
33
+
34
+ def _inner_raise() -> None:
35
+ raise ValueError("boom")
36
+
37
+
38
+ def _raise_boom() -> None:
39
+ _inner_raise()
40
+
41
+
42
+ def record_show_locals(
43
+ monkeypatch: pytest.MonkeyPatch,
44
+ *,
45
+ instance_flag: bool,
46
+ via_handler: bool,
47
+ call_arg: bool | None = None,
48
+ ) -> list[bool | None]:
49
+ """Capture the ``show_locals`` value each ``print_exception`` receives.
50
+
51
+ Rich only emits a per-frame locals section when it can read the frame's
52
+ source file, so asserting on rendered output length is fragile across
53
+ environments (the locals are silently dropped where the source is
54
+ unavailable). These cases exercise the flag-resolution logic instead, so we
55
+ spy on the value handed to rich rather than on what rich renders from it.
56
+ """
57
+ console_err = make_color_console()
58
+ reporter = CliReporter(
59
+ make_console(), debug=True, show_locals=instance_flag, console_err=console_err
60
+ )
61
+ recorded: list[bool | None] = []
62
+
63
+ def spy(*, show_locals: bool | None = None, **_: object) -> None:
64
+ recorded.append(show_locals)
65
+
66
+ monkeypatch.setattr(console_err, "print_exception", spy)
67
+ if via_handler:
68
+ with pytest.raises(SystemExit):
69
+ with reporter.handler():
70
+ _raise_boom()
71
+ else:
72
+ try:
73
+ _raise_boom()
74
+ except ValueError:
75
+ reporter.debug_traceback(show_locals=call_arg)
76
+ return recorded
77
+
78
+
20
79
  @pytest.fixture
21
80
  def console() -> Console:
22
81
  return make_console()
@@ -353,3 +412,119 @@ def test_debug_traceback_noops_outside_except_block(
353
412
  ) -> None:
354
413
  debug_reporter.debug_traceback()
355
414
  assert capsys.readouterr().err == ""
415
+
416
+
417
+ @pytest.mark.parametrize(
418
+ ("instance_flag", "expected"),
419
+ [
420
+ (False, False),
421
+ (True, True),
422
+ ],
423
+ )
424
+ def test_show_locals_via_handler(
425
+ monkeypatch: pytest.MonkeyPatch, instance_flag: bool, expected: bool
426
+ ) -> None:
427
+ recorded = record_show_locals(
428
+ monkeypatch, instance_flag=instance_flag, via_handler=True
429
+ )
430
+ assert recorded == [expected]
431
+
432
+
433
+ @pytest.mark.parametrize(
434
+ ("instance_flag", "call_arg", "expected"),
435
+ [
436
+ (False, True, True),
437
+ (True, False, False),
438
+ (True, None, True),
439
+ (False, None, False),
440
+ ],
441
+ )
442
+ def test_debug_traceback_show_locals_resolution(
443
+ monkeypatch: pytest.MonkeyPatch,
444
+ instance_flag: bool,
445
+ call_arg: bool | None,
446
+ expected: bool,
447
+ ) -> None:
448
+ recorded = record_show_locals(
449
+ monkeypatch, instance_flag=instance_flag, via_handler=False, call_arg=call_arg
450
+ )
451
+ assert recorded == [expected]
452
+
453
+
454
+ def test_debug_traceback_show_locals_short_circuits_when_not_debug() -> None:
455
+ console_err = make_color_console()
456
+ reporter = CliReporter(make_console(), console_err=console_err)
457
+ with console_err.capture() as capture:
458
+ try:
459
+ _raise_boom()
460
+ except ValueError:
461
+ reporter.debug_traceback(show_locals=True)
462
+ assert capture.get() == ""
463
+
464
+
465
+ @pytest.mark.parametrize("no_color", [True, False])
466
+ def test_derived_console_err_inherits_no_color_intent(no_color: bool) -> None:
467
+ # With no ``console_err`` injected, the derived stderr console must carry the
468
+ # injected stdout console's ``no_color`` intent. The parametrized pair proves
469
+ # the flag is load-bearing rather than hard-coded.
470
+ console_err = derive_console_err(make_console(no_color=no_color))
471
+ assert console_err.no_color is no_color
472
+
473
+
474
+ def test_derived_console_err_inherits_overridden_theme_role() -> None:
475
+ # An overridden role on the injected console must re-resolve on the derived
476
+ # stderr console (theme intent, not just no_color, is inherited).
477
+ console = make_console(styles={"misc": "yellow"})
478
+ console_err = derive_console_err(console)
479
+ assert console_err.get_style("misc") == console.get_style("misc")
480
+
481
+
482
+ def test_derived_console_err_does_not_inherit_geometry() -> None:
483
+ # Width is left to Rich's per-stream detection, not copied from the injected
484
+ # console — an artificially narrow injected console must not constrain it.
485
+ console = make_console()
486
+ console.width = 20
487
+ console_err = derive_console_err(console)
488
+ assert console_err.width == make_console(stderr=True).width
489
+
490
+
491
+ def test_derived_console_err_does_not_inherit_color_system() -> None:
492
+ # Color system is left to Rich's per-stream detection, not copied from the
493
+ # injected console — a forced-color injected console must not impose its
494
+ # ``standard`` color system on the derived stderr stream.
495
+ console = make_color_console()
496
+ console_err = derive_console_err(console)
497
+ assert console_err.color_system == make_console(stderr=True).color_system
498
+
499
+
500
+ def test_derived_console_err_tolerates_themeless_console() -> None:
501
+ # The public constructor accepts any Rich console; a themeless one (no role
502
+ # styles defined) must degrade to unstyled roles rather than crash on
503
+ # ``get_style`` during derivation.
504
+ console_err = derive_console_err(Console())
505
+ assert console_err.get_style("misc") == Style()
506
+
507
+
508
+ def test_debug_traceback_emits_full_cause_chain(
509
+ debug_reporter: CliReporter, capsys: pytest.CaptureFixture[str]
510
+ ) -> None:
511
+ # A chained exception raised through ``handler()`` under debug must emit the
512
+ # WHOLE cause chain to stderr, not just the outermost frame. Assert on the
513
+ # two type names (stable) rather than Rich's traceback layout (brittle).
514
+ with pytest.raises(SystemExit):
515
+ with debug_reporter.handler():
516
+ try:
517
+ raise _Inner("inner boom")
518
+ except _Inner as inner:
519
+ raise _Outer("outer boom") from inner
520
+
521
+ err = capsys.readouterr().err
522
+ assert "_Outer" in err
523
+ assert "_Inner" in err
524
+
525
+
526
+ def test_injected_console_err_is_used_as_is() -> None:
527
+ # The explicit ``console_err`` escape hatch bypasses derivation entirely.
528
+ console_err = make_color_console()
529
+ reporter = CliReporter(make_console(no_color=True), console_err=console_err)
530
+ assert reporter.console_err is console_err
@@ -90,7 +90,7 @@ wheels = [
90
90
 
91
91
  [[package]]
92
92
  name = "cli-error"
93
- version = "0.1.0"
93
+ version = "0.1.1"
94
94
  source = { editable = "." }
95
95
  dependencies = [
96
96
  { name = "rich" },
@@ -153,11 +153,11 @@ wheels = [
153
153
 
154
154
  [[package]]
155
155
  name = "filelock"
156
- version = "3.29.4"
156
+ version = "3.29.6"
157
157
  source = { registry = "https://pypi.org/simple" }
158
- sdist = { url = "https://files.pythonhosted.org/packages/e6/dc/be6cbe99670cd6e4ad387123647cb08e0c32975e223f82551e914c5568a6/filelock-3.29.4.tar.gz", hash = "sha256:10cdb3656fc44541cdf30652a93fb10ec6b05325620eb316bd26893e4201538a", size = 63028, upload-time = "2026-06-13T16:12:00.744Z" }
158
+ sdist = { url = "https://files.pythonhosted.org/packages/e0/c8/35bdf04fb30755e2ed758f877edf3eb4a243c2463d3a258cc28b18b7a6e2/filelock-3.29.6.tar.gz", hash = "sha256:895c532ef3f4ef04972b9446a8c4e2931a5c399ff3c4be4c9369f2639b80f793", size = 70301, upload-time = "2026-07-06T23:08:08.577Z" }
159
159
  wheels = [
160
- { url = "https://files.pythonhosted.org/packages/13/37/a065dc3bd6e49423a6532c642ca7378d3f467b1ef44c2800c937af7f9739/filelock-3.29.4-py3-none-any.whl", hash = "sha256:dac1648087d5115554850d113e7dd8c83ab2d38e3435dde2d4f163847e57b767", size = 42757, upload-time = "2026-06-13T16:11:59.582Z" },
160
+ { url = "https://files.pythonhosted.org/packages/bb/49/7467c2946ccd9617f7da38187071bdc45bb9a95df51f4d63d6622432ce4e/filelock-3.29.6-py3-none-any.whl", hash = "sha256:14d5f5597d2e0c4dbd774cfb6d8132da1db44da83732aab679d54f7dcf97ab65", size = 45478, upload-time = "2026-07-06T23:08:07.197Z" },
161
161
  ]
162
162
 
163
163
  [[package]]
@@ -430,15 +430,15 @@ wheels = [
430
430
 
431
431
  [[package]]
432
432
  name = "python-discovery"
433
- version = "1.4.2"
433
+ version = "1.4.3"
434
434
  source = { registry = "https://pypi.org/simple" }
435
435
  dependencies = [
436
436
  { name = "filelock" },
437
437
  { name = "platformdirs" },
438
438
  ]
439
- sdist = { url = "https://files.pythonhosted.org/packages/0b/1a/cbbaf13b730abb0a16b964d984e19f2fe520c21a4dc664051359a3f5a9e7/python_discovery-1.4.2.tar.gz", hash = "sha256:8f3746c4b4968d22afbb97d36e1a0e5b66e6c0f297290f2e95f05b9b8bf18690", size = 70277, upload-time = "2026-06-11T16:10:42.383Z" }
439
+ sdist = { url = "https://files.pythonhosted.org/packages/66/26/8b004cc36f430345136f6f00fa1aa9ed596c8ed1e8504625fa79522ff39c/python_discovery-1.4.3.tar.gz", hash = "sha256:ad57d7045a862460d4a235986c33f13ed707d3aeb9153fa47eb7dfd0d4673289", size = 70438, upload-time = "2026-07-03T13:21:51.621Z" }
440
440
  wheels = [
441
- { url = "https://files.pythonhosted.org/packages/1a/82/a70006589557f267f15bd384c0642ad49f0d97b690c3a05b166b9dcbad3b/python_discovery-1.4.2-py3-none-any.whl", hash = "sha256:475803f53b7b2ed6e490e27373f9d8340f7d2eebf9acdaf645d7d714c97bb500", size = 33886, upload-time = "2026-06-11T16:10:41.192Z" },
441
+ { url = "https://files.pythonhosted.org/packages/28/78/9b77ecb4644d1bbea94d29abf78f21c47eca6eb79e9745b702ec0bed2e19/python_discovery-1.4.3-py3-none-any.whl", hash = "sha256:b6e1e4a7d9e3f6948c39746ffe8218225162d738ba39d05ab1d2f6c1cac4878c", size = 33885, upload-time = "2026-07-03T13:21:50.174Z" },
442
442
  ]
443
443
 
444
444
  [[package]]
@@ -494,7 +494,7 @@ wheels = [
494
494
 
495
495
  [[package]]
496
496
  name = "tox"
497
- version = "4.56.1"
497
+ version = "4.56.2"
498
498
  source = { registry = "https://pypi.org/simple" }
499
499
  dependencies = [
500
500
  { name = "cachetools" },
@@ -508,9 +508,9 @@ dependencies = [
508
508
  { name = "tomli-w" },
509
509
  { name = "virtualenv" },
510
510
  ]
511
- sdist = { url = "https://files.pythonhosted.org/packages/60/6a/d20d405fc6661902ff803a9fa048d8aae27597c3b5dc750369ded82d08f7/tox-4.56.1.tar.gz", hash = "sha256:db1c2610802553189cf40de251661d066a635ee0ed9bf2a60093b5f1a7f36ef8", size = 283155, upload-time = "2026-06-25T06:18:36.802Z" }
511
+ sdist = { url = "https://files.pythonhosted.org/packages/a4/be/0a4370df64b4610c653482dd55e0a6818fa31c551fce7659c253e3668638/tox-4.56.2.tar.gz", hash = "sha256:346442f3982a5c1b792abcbc476a90f821031154251ea3506401c4c51c62cc2a", size = 286251, upload-time = "2026-07-07T02:11:16.729Z" }
512
512
  wheels = [
513
- { url = "https://files.pythonhosted.org/packages/d8/97/560a5dfde154619d9643b1e208119dddc29bbb35a38a4ce4d095c16cf8f0/tox-4.56.1-py3-none-any.whl", hash = "sha256:4d06b925c4dd67872099b39c5a46fba79a2169c5f6e32060f95a8b1181f0ef55", size = 216469, upload-time = "2026-06-25T06:18:35.229Z" },
513
+ { url = "https://files.pythonhosted.org/packages/03/66/34fdfb42ff381da15404bf8aa92b1b0e6ddad7263e4ec23bee3daf359d50/tox-4.56.2-py3-none-any.whl", hash = "sha256:245b60f2610e126070b289a862d6288401a387147e86a30c9316cb0ab276137b", size = 217329, upload-time = "2026-07-07T02:11:15.04Z" },
514
514
  ]
515
515
 
516
516
  [[package]]
@@ -549,33 +549,33 @@ wheels = [
549
549
 
550
550
  [[package]]
551
551
  name = "uv"
552
- version = "0.11.26"
553
- source = { registry = "https://pypi.org/simple" }
554
- sdist = { url = "https://files.pythonhosted.org/packages/d0/cb/5efc713948ddb10b00abfb51bfd429221c720175557f9c7965fea2448fe4/uv-0.11.26.tar.gz", hash = "sha256:2a433ece2ace088dd572d8abb0e6bd9a4ecb0e10bc9856447bbb37545f384f29", size = 4331220, upload-time = "2026-06-30T14:52:03.77Z" }
555
- wheels = [
556
- { url = "https://files.pythonhosted.org/packages/c2/71/86dbffac9e26df28a16639c426cf4ba572aaf43d9231463e0dca337895b2/uv-0.11.26-py3-none-linux_armv6l.whl", hash = "sha256:fb97bf04512dfe16d86084e75d8129701fc8da9fb40de8746b73c3aa617c5897", size = 25197324, upload-time = "2026-06-30T14:50:51.75Z" },
557
- { url = "https://files.pythonhosted.org/packages/ec/80/525b73c8188e7052343e7109466a08fcd5195055aff4b0346ce3622e48cb/uv-0.11.26-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a58a06e5a4b0035538d3ab4160ad74c716076ea7148eb3317171c6276ac020b4", size = 24179172, upload-time = "2026-06-30T14:50:56.52Z" },
558
- { url = "https://files.pythonhosted.org/packages/7b/5e/cf7b94ed3b1932c2a62573dcd388ad6c1da5c52111cd71ab7f20faa4a0aa/uv-0.11.26-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7b6d078d2ce83897884c2330c0676f27be4bf3d223fb2a409460f579fb5f0a98", size = 22949576, upload-time = "2026-06-30T14:51:00.538Z" },
559
- { url = "https://files.pythonhosted.org/packages/bf/fd/71fa021f6909c4139d8354bea623b5e0ef0ce4a08da250da1a1645528da2/uv-0.11.26-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:1cd9ba4951681ce17f1703106266fcbe27aaa7d37f07d53cce8b5686d68a8755", size = 24936673, upload-time = "2026-06-30T14:51:04.496Z" },
560
- { url = "https://files.pythonhosted.org/packages/7d/5e/273425e58a8812423e3d1f6c5da1015e636fbf13a83d104317ca37e16304/uv-0.11.26-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:e4f4c3268e69ac96f01972274a62f5f930c03cbc680adba6f21e63237ba3a639", size = 24719617, upload-time = "2026-06-30T14:51:08.419Z" },
561
- { url = "https://files.pythonhosted.org/packages/81/f8/1601e2acc7c54963814b4831eab996d8599e690712722c5acec5114860be/uv-0.11.26-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:efcbe0e187846f5ddba23bcaed17e4f9cd2463da5c45bdb5869616f686d713ff", size = 24734176, upload-time = "2026-06-30T14:51:12.685Z" },
562
- { url = "https://files.pythonhosted.org/packages/88/d2/a8a422e54c08cf4b8d51bedb9dbdd3cc233aa290ad8b3ee0438c0c02a3a5/uv-0.11.26-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:120ab2de93164d08cf5950f7fe18cbebe3ff670865ae41a292452bab2346477f", size = 26158780, upload-time = "2026-06-30T14:51:16.514Z" },
563
- { url = "https://files.pythonhosted.org/packages/db/e6/647fe5fdc888a3d27f79977877ce4e88052fe9be5398371e51bb134fc262/uv-0.11.26-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9052bf27c7ee426901f35a48715fa9288ce631c1878b91c9a6c950288f4b8633", size = 27009550, upload-time = "2026-06-30T14:51:20.659Z" },
564
- { url = "https://files.pythonhosted.org/packages/72/c2/85d8e762ad83b0f14fae2255b0578c4fd7dc915746f81b64ed786342627a/uv-0.11.26-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:efdddfcc9b1b790c5f7985c5c183c851682ced165b44ffa914f4947f5cad1fbf", size = 26183777, upload-time = "2026-06-30T14:51:24.715Z" },
565
- { url = "https://files.pythonhosted.org/packages/d3/00/478c3a870dcac690b8c337ee950a60a952e817f574945e85155c3cc0ab34/uv-0.11.26-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4dcf4e0b5b5cbdc242dcb002f1f8d99e7cf8c043609869228a9ce15e095c0b18", size = 26260589, upload-time = "2026-06-30T14:51:28.809Z" },
566
- { url = "https://files.pythonhosted.org/packages/a7/51/e4e43e106fb8cdc026b97491ea4600f4194a9c4da0b4e4e30c2a7dceb268/uv-0.11.26-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:866ae8d28f7381c15de0906a284c1e97916424c635bf40f7960b3fc889cd725e", size = 25073850, upload-time = "2026-06-30T14:51:32.717Z" },
567
- { url = "https://files.pythonhosted.org/packages/f2/c2/e772b7e6c8a835e8bf6739a391cdfc8e8e244c5c496d9b40625068b59ff4/uv-0.11.26-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:22f6d62e794b252ff3a1e2dfe5010cc76208f90b2c906e54971a0223ad6f16bc", size = 25682609, upload-time = "2026-06-30T14:51:36.888Z" },
568
- { url = "https://files.pythonhosted.org/packages/1a/69/ea77209a224a23a399cb7f6414f77ef032bd9e083e01199a0ebebf0d3ff2/uv-0.11.26-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:edd0c12b75141a6d830d138a91e366ad66e630f1c1dcaf83b8325b80cbacfcbb", size = 25800556, upload-time = "2026-06-30T14:51:40.937Z" },
569
- { url = "https://files.pythonhosted.org/packages/77/60/b6c0c03d2538a016b6624fa251960012e564ea02f841e958c7d60e974685/uv-0.11.26-py3-none-musllinux_1_1_i686.whl", hash = "sha256:af6a45b11a569cc4d2437e89a25a53dcf753f2a02a8f2de96be09b9b942cb3ec", size = 25385658, upload-time = "2026-06-30T14:51:45.103Z" },
570
- { url = "https://files.pythonhosted.org/packages/8d/e7/46881ff9164aa2e7c649901837d58eee3c57beb3b0fcc0fea6a4e40cf8f3/uv-0.11.26-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:c28822517d03aebbe9549aaaecc88ad580e4b2b6a927abffe5774a74d6ba09f6", size = 26551013, upload-time = "2026-06-30T14:51:49.062Z" },
571
- { url = "https://files.pythonhosted.org/packages/d6/94/380dad6c2bbe12417025aacd12cfc08322ed4c9dd8f760bff7035b86f22d/uv-0.11.26-py3-none-win32.whl", hash = "sha256:79e5c1b3410047e1962290c3b7b8f512d2c1bb95200c60b016f7729287cf34c0", size = 23947180, upload-time = "2026-06-30T14:51:53.065Z" },
572
- { url = "https://files.pythonhosted.org/packages/d0/13/9c588226d5b478328d739e654944430719f3ffe8999d6a24d425ec9664ab/uv-0.11.26-py3-none-win_amd64.whl", hash = "sha256:d95567e9470dc48ff03265f420c3c6973f6437f18a79d5e00b6eb4b2d9379907", size = 26909320, upload-time = "2026-06-30T14:51:57.235Z" },
573
- { url = "https://files.pythonhosted.org/packages/21/1d/ea66b12813878797126e2b3aca124b1c9c5ef53120702d1c00172f90a21d/uv-0.11.26-py3-none-win_arm64.whl", hash = "sha256:7e69d1569afbb936e7bf4e4ab2f72d606405f4a68f380f088a0b2233e84e056a", size = 25176820, upload-time = "2026-06-30T14:52:01.05Z" },
552
+ version = "0.11.27"
553
+ source = { registry = "https://pypi.org/simple" }
554
+ sdist = { url = "https://files.pythonhosted.org/packages/8f/ed/b8bb8bf2044561e3349ebe75be0d8abdeb19577886753b49200ec6caea0d/uv-0.11.27.tar.gz", hash = "sha256:3469204521869f0e6bdea17b02c1d86db2d0150820895653a6152cab206fb00b", size = 5978029, upload-time = "2026-07-06T21:00:29.995Z" }
555
+ wheels = [
556
+ { url = "https://files.pythonhosted.org/packages/b2/fe/8a080fb603f276ceb2d960454b2b5998cc6e5c0c6d5dbbbeb48b141de4f2/uv-0.11.27-py3-none-linux_armv6l.whl", hash = "sha256:6cedad2185b16bd5da77096d4813159c9c3c572897537256eecdb4092941adc0", size = 25490659, upload-time = "2026-07-06T20:59:29.003Z" },
557
+ { url = "https://files.pythonhosted.org/packages/d1/15/886a2ef7b00c1a4fcf706e7a6b946b3f7485a55f42d0aa9b5572caed58b6/uv-0.11.27-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:42b273459864c67f3736ae2a63929df463975d885bbc1afd8f53f7e0fb1f2269", size = 24498434, upload-time = "2026-07-06T20:59:33.042Z" },
558
+ { url = "https://files.pythonhosted.org/packages/2d/6e/45335717c4372bf955c87bc05c6462dd4c0c6087ed12c3991b14cbba77cd/uv-0.11.27-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b316464a1028a43478b74424b4b78925c463758baec93cb754fb2392ba175138", size = 23248727, upload-time = "2026-07-06T20:59:36.201Z" },
559
+ { url = "https://files.pythonhosted.org/packages/0a/fa/41dde1e350c5a277b797371faf96fb013c2c13ed18df6efc51ed8ec9bb5b/uv-0.11.27-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:3ca478883d78270ceed57f229d1b480dbcadf5d79b2ce9624574807d6ec48554", size = 25208126, upload-time = "2026-07-06T20:59:39.984Z" },
560
+ { url = "https://files.pythonhosted.org/packages/00/e3/bc6d4af24ab533db4c741cebade3a58b91fc0563059d39e838161dd210c8/uv-0.11.27-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:232dfb7549c8a4217ab1970dea0de731926b8ca21769ed89951ad8f8c3bce8b6", size = 25030974, upload-time = "2026-07-06T20:59:43.378Z" },
561
+ { url = "https://files.pythonhosted.org/packages/c3/0c/82e736930d3f78ca6c3286ef31627d7155e20191af88afc40ec29486f400/uv-0.11.27-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4c0cf7c7960587a231252cbe4d7a032b5a02c29cfab3fcc4e75a915b1c5cacee", size = 25015115, upload-time = "2026-07-06T20:59:46.694Z" },
562
+ { url = "https://files.pythonhosted.org/packages/a0/5b/512ebf3b24b708ea2adda4ec4d4bd23b2404d7a7ea3bd24d2b92210ab680/uv-0.11.27-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47b5863ef715ffe4c29e792c9884a4e8b8016b0628d3613ac84c696acc128a78", size = 26487914, upload-time = "2026-07-06T20:59:50.414Z" },
563
+ { url = "https://files.pythonhosted.org/packages/98/d5/3871185331266156fbd09ab0ad7ddc69ca759b3a28ece5488bdb64f152d9/uv-0.11.27-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a5764155d2a578a41d2827f464d4ce5b7788632e732cb304e2025fb3d043b46f", size = 27393306, upload-time = "2026-07-06T20:59:53.852Z" },
564
+ { url = "https://files.pythonhosted.org/packages/24/0f/9fe3cc9b8d498f2e426b3cda90d337b076d64f19c2d55cb0b7d091c4bfe9/uv-0.11.27-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d806e1067c8eb9f8ba5dbdca46484512d224070e8473442319d4c1fbc3246f4d", size = 26563495, upload-time = "2026-07-06T20:59:56.945Z" },
565
+ { url = "https://files.pythonhosted.org/packages/e7/be/18393c15ac0c982fefa34944135ead3adc565da5023d973eff5081a95a62/uv-0.11.27-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cb783b9db3868e51e8da1956aaa7e800262ad1af03dfc8b024f3b04ecb447c2", size = 26652374, upload-time = "2026-07-06T21:00:00.256Z" },
566
+ { url = "https://files.pythonhosted.org/packages/b8/93/1bb8bee2b0bcc6e9655518ecaa9a792b1952dd99ae7366fe4f6c82754361/uv-0.11.27-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:8e2b86d3dee515eeba6ea001d8b3eac20b204db07d39b04187c755a490a4fba6", size = 25341573, upload-time = "2026-07-06T21:00:03.789Z" },
567
+ { url = "https://files.pythonhosted.org/packages/e6/e0/fae0d1c972b25ac192ac807c1b1baf0f12070c40c8af2eb8e22310ea8846/uv-0.11.27-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:d325b8bd2665ce3ac58d33f977683cfb2cb8eb951effab06fad8518426bc0a90", size = 26051363, upload-time = "2026-07-06T21:00:07.281Z" },
568
+ { url = "https://files.pythonhosted.org/packages/e5/a2/4c0c103ea431f5d70cd33f10c754b5b1de9687fea982ca1a20a4eee0a4a7/uv-0.11.27-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:3d5c9036b558a323f254e3d16038183c22c55e0d06b921f1ed9706953311e52f", size = 26148896, upload-time = "2026-07-06T21:00:10.56Z" },
569
+ { url = "https://files.pythonhosted.org/packages/bb/b7/a514307a90ca9f1cb59efd0898958dea6c1092ad5e4e6b6f2eb4d55109db/uv-0.11.27-py3-none-musllinux_1_1_i686.whl", hash = "sha256:4cc04e625e7bfac6975fd384d6598169cea0030995ff3b557950ec3077974a4d", size = 25701138, upload-time = "2026-07-06T21:00:13.772Z" },
570
+ { url = "https://files.pythonhosted.org/packages/bf/46/c6cfa675d727a199cfce32f7a30350ce6e47f699c3cd7f0433a619c671d4/uv-0.11.27-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:109d8e145b390d0e14f2aefd9708a7522a4236d6bee20361a7558e77a14c0531", size = 26913232, upload-time = "2026-07-06T21:00:17.233Z" },
571
+ { url = "https://files.pythonhosted.org/packages/a4/04/0070ecdea37baa9d38187abedc78eed9a52a2e6cb1d4f175c580640f3456/uv-0.11.27-py3-none-win32.whl", hash = "sha256:b330d2ab6af0f79396fb350a9fed8ed1aa92516310f7533de9a8dfebc4e147cd", size = 24374672, upload-time = "2026-07-06T21:00:20.399Z" },
572
+ { url = "https://files.pythonhosted.org/packages/d0/b9/f3df522d2e609fe511e5bee24b34c91ce281ce472594121fa1d5fa159d9d/uv-0.11.27-py3-none-win_amd64.whl", hash = "sha256:e20f25921ed4c46d32ac18a9b2e610c0384439fd367f647f2b68e0e1d90e9885", size = 27275452, upload-time = "2026-07-06T21:00:23.903Z" },
573
+ { url = "https://files.pythonhosted.org/packages/8d/2e/4ea9c3bbf95a21c627a3dfa359f5a3266e648cd103c5ade374a6e4265c5f/uv-0.11.27-py3-none-win_arm64.whl", hash = "sha256:a41da27667e95c0df939e99e77a4d9169fb895df0f6788b2ab65ed5efdc1395f", size = 25561477, upload-time = "2026-07-06T21:00:27.333Z" },
574
574
  ]
575
575
 
576
576
  [[package]]
577
577
  name = "virtualenv"
578
- version = "21.5.1"
578
+ version = "21.6.0"
579
579
  source = { registry = "https://pypi.org/simple" }
580
580
  dependencies = [
581
581
  { name = "distlib" },
@@ -583,7 +583,7 @@ dependencies = [
583
583
  { name = "platformdirs" },
584
584
  { name = "python-discovery" },
585
585
  ]
586
- sdist = { url = "https://files.pythonhosted.org/packages/f1/a5/81f987504738e6defeed61ec1c47e2aefab3c35d8eeb87e1b3f38cf28254/virtualenv-21.5.1.tar.gz", hash = "sha256:dca3bf98275a59c652b69d68e73433e597d977c2da9198882479d1a7188009c8", size = 4578798, upload-time = "2026-06-16T16:23:58.603Z" }
586
+ sdist = { url = "https://files.pythonhosted.org/packages/74/65/ec1d92091671e6407d3e7c1f5801413bb7b2b57630a50cae7750456ba0ed/virtualenv-21.6.0.tar.gz", hash = "sha256:e18a4d750f2b64dea73e72ffde3922f3c52365fabdc8157ebd3da20d031c4734", size = 5526111, upload-time = "2026-07-06T22:49:56.972Z" }
587
587
  wheels = [
588
- { url = "https://files.pythonhosted.org/packages/2c/02/3623e6169bed617ed1e2d372f7c69f92ec28d54c4dfc997055c8578ec148/virtualenv-21.5.1-py3-none-any.whl", hash = "sha256:55aa670b67bbfb991b03fda39bd3276d92c419d702376e98c5df1c9989a26783", size = 4558820, upload-time = "2026-06-16T16:23:56.963Z" },
588
+ { url = "https://files.pythonhosted.org/packages/b8/e7/2fbd0cc1653c53eed8f10670538bb547de2b3e37aacad283faa82a71094b/virtualenv-21.6.0-py3-none-any.whl", hash = "sha256:bce9d097950fef9d81129b333babfb7767072850c2f1acce0ec536708401bfd1", size = 5506216, upload-time = "2026-07-06T22:49:54.941Z" },
589
589
  ]
@@ -1 +0,0 @@
1
- {"sessionId":"d6c578d2-0e67-41a3-9ab3-6eb663d5b9d5","pid":25,"procStart":"26355062","acquiredAt":1783286663093}
@@ -1,16 +0,0 @@
1
- s01t01
2
- s01t02
3
- s01t10
4
- s01t11
5
- s01t12
6
- s01t03
7
- s01t13
8
- s01t04
9
- s01t05
10
- s01t14
11
- s01t06
12
- s01t07
13
- s01t20
14
- s01t18
15
- s01t08
16
- s01t09
@@ -1 +0,0 @@
1
- s01t20
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