cli-error 0.1.0__tar.gz → 0.1.2__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 (72) hide show
  1. {cli_error-0.1.0 → cli_error-0.1.2}/.github/workflows/ci.yml +2 -2
  2. {cli_error-0.1.0 → cli_error-0.1.2}/.github/workflows/release.yml +2 -2
  3. {cli_error-0.1.0 → cli_error-0.1.2}/.sandbox/README.md +0 -4
  4. {cli_error-0.1.0 → cli_error-0.1.2}/.sandbox/docker-compose.yml +0 -1
  5. {cli_error-0.1.0 → cli_error-0.1.2}/.tasker/s01-extract-repo-skills-error-reporting/README.md +7 -5
  6. {cli_error-0.1.0 → cli_error-0.1.2}/.tasker/s01-extract-repo-skills-error-reporting/s01t15-reconsider-derive-debug-stderr-console.md +1 -1
  7. {cli_error-0.1.0 → cli_error-0.1.2}/.tasker/s01-extract-repo-skills-error-reporting/s01t16-optionally-show-locals-in-debug.md +1 -1
  8. {cli_error-0.1.0 → cli_error-0.1.2}/.tasker/s01-extract-repo-skills-error-reporting/s01t17-test-debug-traceback-includes-the.md +1 -1
  9. {cli_error-0.1.0 → cli_error-0.1.2}/.tasker/s01-extract-repo-skills-error-reporting/s01t19-consolidate-shared-test-helpers-across.md +1 -1
  10. cli_error-0.1.2/.tasker/s01-extract-repo-skills-error-reporting/s01t21-reconsider-debug-output-layout-evaluate.md +33 -0
  11. cli_error-0.1.2/.tasker/s01-extract-repo-skills-error-reporting/s01t22-unify-debug-helper-label-idiom.md +61 -0
  12. {cli_error-0.1.0 → cli_error-0.1.2}/PKG-INFO +1 -1
  13. {cli_error-0.1.0 → cli_error-0.1.2}/README.md +27 -6
  14. {cli_error-0.1.0 → cli_error-0.1.2}/pyproject.toml +1 -1
  15. {cli_error-0.1.0 → cli_error-0.1.2}/src/cli_error/_console.py +11 -0
  16. {cli_error-0.1.0 → cli_error-0.1.2}/src/cli_error/_reporter.py +28 -29
  17. cli_error-0.1.2/tests/helpers.py +33 -0
  18. {cli_error-0.1.0 → cli_error-0.1.2}/tests/test_console.py +13 -39
  19. {cli_error-0.1.0 → cli_error-0.1.2}/tests/test_errors.py +2 -3
  20. {cli_error-0.1.0 → cli_error-0.1.2}/tests/test_render.py +4 -19
  21. {cli_error-0.1.0 → cli_error-0.1.2}/tests/test_reporter.py +208 -11
  22. {cli_error-0.1.0 → cli_error-0.1.2}/uv.lock +68 -67
  23. cli_error-0.1.0/.claude/scheduled_tasks.lock +0 -1
  24. cli_error-0.1.0/.tasker/.closed +0 -16
  25. cli_error-0.1.0/.tasker/.recent +0 -1
  26. {cli_error-0.1.0 → cli_error-0.1.2}/.flake8 +0 -0
  27. {cli_error-0.1.0 → cli_error-0.1.2}/.gitignore +0 -0
  28. {cli_error-0.1.0 → cli_error-0.1.2}/.mcp.json +0 -0
  29. {cli_error-0.1.0 → cli_error-0.1.2}/.sandbox/.env.example +0 -0
  30. {cli_error-0.1.0 → cli_error-0.1.2}/.sandbox/base/Dockerfile +0 -0
  31. {cli_error-0.1.0 → cli_error-0.1.2}/.sandbox/base/tmux.conf +0 -0
  32. {cli_error-0.1.0 → cli_error-0.1.2}/.sandbox/base/zshrc +0 -0
  33. {cli_error-0.1.0 → cli_error-0.1.2}/.sandbox/run.sh +0 -0
  34. {cli_error-0.1.0 → cli_error-0.1.2}/.sandbox/scripts/_lib.sh +0 -0
  35. {cli_error-0.1.0 → cli_error-0.1.2}/.sandbox/scripts/attach.sh +0 -0
  36. {cli_error-0.1.0 → cli_error-0.1.2}/.sandbox/scripts/build.sh +0 -0
  37. {cli_error-0.1.0 → cli_error-0.1.2}/.sandbox/scripts/claude.sh +0 -0
  38. {cli_error-0.1.0 → cli_error-0.1.2}/.sandbox/scripts/shell.sh +0 -0
  39. {cli_error-0.1.0 → cli_error-0.1.2}/.sandbox/variants/python/Dockerfile +0 -0
  40. {cli_error-0.1.0 → cli_error-0.1.2}/.sandbox/variants/python/compose.override.yml +0 -0
  41. {cli_error-0.1.0 → cli_error-0.1.2}/.sandbox/worktree_run.sh +0 -0
  42. {cli_error-0.1.0 → cli_error-0.1.2}/.sandbox/worktree_shell.sh +0 -0
  43. {cli_error-0.1.0 → cli_error-0.1.2}/.tasker/.gitignore +0 -0
  44. {cli_error-0.1.0 → cli_error-0.1.2}/.tasker/archive/.gitkeep +0 -0
  45. {cli_error-0.1.0 → cli_error-0.1.2}/.tasker/s01-extract-repo-skills-error-reporting/s01t01-rename-package-structure-clierror-cliexit.md +0 -0
  46. {cli_error-0.1.0 → cli_error-0.1.2}/.tasker/s01-extract-repo-skills-error-reporting/s01t02-fluent-builder-for-clierror-context.md +0 -0
  47. {cli_error-0.1.0 → cli_error-0.1.2}/.tasker/s01-extract-repo-skills-error-reporting/s01t03-render-formatted-error.md +0 -0
  48. {cli_error-0.1.0 → cli_error-0.1.2}/.tasker/s01-extract-repo-skills-error-reporting/s01t04-error-handler-errorreporter-context-manager.md +0 -0
  49. {cli_error-0.1.0 → cli_error-0.1.2}/.tasker/s01-extract-repo-skills-error-reporting/s01t05-cause-chain-rendering.md +0 -0
  50. {cli_error-0.1.0 → cli_error-0.1.2}/.tasker/s01-extract-repo-skills-error-reporting/s01t06-debug-traceback.md +0 -0
  51. {cli_error-0.1.0 → cli_error-0.1.2}/.tasker/s01-extract-repo-skills-error-reporting/s01t07-console-creation-with-style-overrides.md +0 -0
  52. {cli_error-0.1.0 → cli_error-0.1.2}/.tasker/s01-extract-repo-skills-error-reporting/s01t08-library-documentation-readme.md +0 -0
  53. {cli_error-0.1.0 → cli_error-0.1.2}/.tasker/s01-extract-repo-skills-error-reporting/s01t09-deployment-setup-ci-release-workflows.md +0 -0
  54. {cli_error-0.1.0 → cli_error-0.1.2}/.tasker/s01-extract-repo-skills-error-reporting/s01t10-unify-markup-construction-template-args.md +0 -0
  55. {cli_error-0.1.0 → cli_error-0.1.2}/.tasker/s01-extract-repo-skills-error-reporting/s01t11-refine-detail-single-last-wins.md +0 -0
  56. {cli_error-0.1.0 → cli_error-0.1.2}/.tasker/s01-extract-repo-skills-error-reporting/s01t12-revisit-props-markup-first-prop.md +0 -0
  57. {cli_error-0.1.0 → cli_error-0.1.2}/.tasker/s01-extract-repo-skills-error-reporting/s01t13-store-detail-as-ready-to.md +0 -0
  58. {cli_error-0.1.0 → cli_error-0.1.2}/.tasker/s01-extract-repo-skills-error-reporting/s01t14-handle-empty-message-cause-in.md +0 -0
  59. {cli_error-0.1.0 → cli_error-0.1.2}/.tasker/s01-extract-repo-skills-error-reporting/s01t18-add-debug-gated-diagnostic-helpers.md +0 -0
  60. {cli_error-0.1.0 → cli_error-0.1.2}/.tasker/s01-extract-repo-skills-error-reporting/s01t20-add-a-safe-post-construction.md +0 -0
  61. {cli_error-0.1.0 → cli_error-0.1.2}/CLAUDE.md +0 -0
  62. {cli_error-0.1.0 → cli_error-0.1.2}/CONTEXT.md +0 -0
  63. {cli_error-0.1.0 → cli_error-0.1.2}/docs/adr/0001-error-formatting-and-theming.md +0 -0
  64. {cli_error-0.1.0 → cli_error-0.1.2}/docs/adr/0002-cli-reporter-output-facade.md +0 -0
  65. {cli_error-0.1.0 → cli_error-0.1.2}/docs/agents/dev-loop.md +0 -0
  66. {cli_error-0.1.0 → cli_error-0.1.2}/docs/agents/task-tracker.md +0 -0
  67. {cli_error-0.1.0 → cli_error-0.1.2}/src/cli_error/__init__.py +0 -0
  68. {cli_error-0.1.0 → cli_error-0.1.2}/src/cli_error/_errors.py +0 -0
  69. {cli_error-0.1.0 → cli_error-0.1.2}/src/cli_error/_render.py +0 -0
  70. {cli_error-0.1.0 → cli_error-0.1.2}/src/cli_error/py.typed +0 -0
  71. {cli_error-0.1.0 → cli_error-0.1.2}/tests/__init__.py +0 -0
  72. {cli_error-0.1.0 → cli_error-0.1.2}/tox.ini +0 -0
@@ -20,10 +20,10 @@ jobs:
20
20
  python-version: ["3.12", "3.13", "3.14"]
21
21
 
22
22
  steps:
23
- - uses: actions/checkout@v4
23
+ - uses: actions/checkout@v5
24
24
 
25
25
  - name: Set up uv
26
- uses: astral-sh/setup-uv@v5
26
+ uses: astral-sh/setup-uv@v6
27
27
  with:
28
28
  enable-cache: true
29
29
  cache-dependency-glob: uv.lock
@@ -13,10 +13,10 @@ jobs:
13
13
  id-token: write
14
14
  contents: read
15
15
  steps:
16
- - uses: actions/checkout@v4
16
+ - uses: actions/checkout@v5
17
17
 
18
18
  - name: Set up uv
19
- uses: astral-sh/setup-uv@v5
19
+ uses: astral-sh/setup-uv@v6
20
20
  with:
21
21
  enable-cache: true
22
22
  cache-dependency-glob: uv.lock
@@ -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,11 @@ 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
62
+ - [x] [s01t21](s01t21-reconsider-debug-output-layout-evaluate.md): Reconsider debug_output layout — rule/divider delimiters
63
+ - [x] [s01t22](s01t22-unify-debug-helper-label-idiom.md): Unify debug-helper label idiom (debug_cmd vs debug_output)
@@ -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
@@ -0,0 +1,33 @@
1
+ ---
2
+ id: s01t21
3
+ slug: reconsider-debug-output-layout-evaluate
4
+ status: done
5
+ ---
6
+
7
+ # Reconsider debug_output layout — rule/divider delimiters
8
+
9
+ **Superseded by s01t22:** the `─── stdout ───` rule layout below is replaced by the uppercase `LABEL:` header idiom (`STDOUT:` / `STDERR:` header line followed by the captured block).
10
+
11
+ `src/cli_error/_reporter.py` · `debug_output()`:
12
+ > TODO: this layout is ugly, lets prepend each line 'stdout: ' prefix
13
+
14
+ The current layout emits a ` stdout:` / ` stderr:` header line followed by the captured text as one escaped `misc` block. The body sits flush at column 0, so it has no visual boundary and merges with surrounding debug output.
15
+
16
+ **Decision:** Adopt the **rule/divider** layout (variant B). Each non-empty stream is delimited by a labeled `misc`-styled rule above the captured block:
17
+
18
+ ```
19
+ ─── stdout ───
20
+ line1
21
+ line2
22
+ ─── stderr ───
23
+ err1
24
+ ```
25
+
26
+ This labels each stream once (no per-line prefix), so it does not reopen the objection s01t18 named when it **rejected** per-line prefixing: *"bloats multi-line logs"*.
27
+
28
+ **Requirements:**
29
+ - A stream's rule is emitted **only when that stream is non-empty** — the existing empty/whitespace-only skip (`rstrip` → `continue`) must still apply, so an empty `stderr` produces no `stderr` rule at all.
30
+ - Keep the captured text as one escaped `misc` block (internal newlines preserved, trailing whitespace `rstrip`ped) — same escaped-arg contract as today.
31
+ - The rule/label styling routes through the existing `misc` role like the rest of the debug helpers.
32
+
33
+ Remove the TODO in `debug_output` once implemented.
@@ -0,0 +1,61 @@
1
+ ---
2
+ id: s01t22
3
+ slug: unify-debug-helper-label-idiom
4
+ status: done
5
+ ---
6
+
7
+ # Unify debug-helper label idiom (debug_cmd vs debug_output)
8
+
9
+ ## Context
10
+
11
+ After s01t21 adopted a labeled rule/divider for `debug_output` (`─── stdout ───`), the two subprocess debug helpers in the reporter presented labels in two different visual idioms: `debug_output` used rule headings while `debug_cmd` used an indented `COMMAND:` / ` cwd:` header. A `--debug` dump commonly interleaves both helpers (a command followed by its captured output), so the two grammars sat side by side in one stderr stream. This task unifies them onto a single label grammar.
12
+
13
+ ## Decisions
14
+
15
+ - **Unify on one uppercase `LABEL:` grammar (variant c)** — abandon the rule/divider entirely; every field is introduced by an all-uppercase, flush-left `LABEL:`. Scalars put the value inline (`RUN: git commit -m 'a b'`, `CWD: /tmp/x`); blocks put a bare header then the value on following lines (`STDOUT:` then the block). One grammar for the whole debug stream; each helper reads fine alone or back-to-back. *Rejected: (a) unifying toward the rule idiom `─── command ───` — a rule above a single-line command is visual noise, and a lonely `─── cwd ───` for one path is heavy; (b) keeping the two idioms distinct — the scalar-vs-block distinction is real but not worth making the reader parse two label grammars in one stream.*
16
+
17
+ - **Label set: `RUN:` / `CWD:` / `STDOUT:` / `STDERR:`** — all-uppercase, column 0, no indentation (drops the old ` cwd:` two-space indent). `RUN` over `COMMAND` (shorter; reads as "the command we ran"). Every line stays wrapped in `[misc]…[/misc]`; the `LABEL:` chars live in the trusted template and values (`cmd`, `cwd`, `text`) pass as escaped args — escaped-arg contract (ADR 0001) unchanged.
18
+
19
+ - **`debug_cmd`: two independent prints** — `RUN: {cmd}` always (via `shlex.join`), then `CWD: {cwd}` only when `cwd is not None`. Two separate `debug_print` calls because `CWD` is conditional. The helpers stay two fully independent methods — `debug_cmd` alone emits just `RUN:`/`CWD:`, `debug_output` alone emits just the stream blocks, and together they compose; no merging.
20
+
21
+ - **`debug_output`: header + block as one `[misc]` unit** — emit each non-empty stream as a single markup string `"[misc]STDOUT:\n{text}[/misc]"` (label and block coupled as one styled unit) rather than two prints. Empty-stream skip preserved unchanged: a stream's header is emitted only when non-empty (`rstrip()` → `continue`); body keeps internal newlines, `rstrip`s trailing whitespace, one escaped `misc` block.
22
+
23
+ - **s01t21 superseded on layout** — this task lands the final unified idiom across both helpers and replaces s01t21's `─── stdout ───` rule; s01t21 (in-review) gets a one-line supersede note pointing here rather than being reopened. *Note: this does NOT reopen the s01t18 objection that rejected per-line `stdout: ` prefixing for bloating multi-line logs — the format is a single header per stream, not a per-line prefix.*
24
+
25
+ ## Edge cases
26
+
27
+ - `debug_cmd` with no `cwd` → only the `RUN:` line, no `CWD:` line.
28
+ - `debug_output` with one empty stream → only the non-empty stream's header+block; an empty/whitespace-only stream produces no header at all (existing `rstrip` → `continue`).
29
+ - Both streams empty/whitespace-only → nothing emitted.
30
+ - All helpers are silent no-ops when `debug` is unset.
31
+ - Markup in values (`[red]…[/red]`, brackets in a command arg) must render literally — escaped-arg contract holds for `cmd`, `cwd`, and `text`.
32
+ - `misc` styling must actually apply: with color forced on, the `[misc]` (=dim) ANSI escape must wrap `RUN:` and the `STDOUT:` header.
33
+ - Multi-line stdout with trailing blank lines → internal newlines preserved, trailing whitespace `rstrip`ped.
34
+
35
+ ## Key files
36
+
37
+ - `src/cli_error/_reporter.py` — `debug_cmd()` (`COMMAND:`/` cwd:` → `RUN:`/`CWD:`) and `debug_output()` (rule → `STDOUT:`/`STDERR:` header block); update `debug_output`'s docstring.
38
+ - `tests/test_reporter.py` — update assertions: `COMMAND:`→`RUN:`, `cwd:`→`CWD:`, `─── stdout ───`→`STDOUT:` (incl. the ordering assertions and the `misc`-style ANSI-escape tests).
39
+ - `.tasker/s01-extract-repo-skills-error-reporting/s01t21-reconsider-debug-output-layout-evaluate.md` — one-line supersede note.
40
+
41
+ ## Acceptance criteria
42
+
43
+ - `debug_cmd(["git","commit","-m","a b"], cwd=Path("/tmp/x"))` under debug emits `RUN: git commit -m 'a b'` and `CWD: /tmp/x`; no `COMMAND:` or lowercase `cwd:`.
44
+ - `debug_cmd(["ls"])` emits `RUN: ls` and no `CWD:` line.
45
+ - `debug_output("out1\nout2 \n", "err1")` emits a `STDOUT:` header before `out1\nout2` and a `STDERR:` header before `err1`; no `─── … ───` rule, no lowercase `stdout:`/`stderr:`.
46
+ - `debug_output("kept", " ")` emits `STDOUT:` + `kept` and no `STDERR:`.
47
+ - Markup-bearing values render literally (`[red]…[/red]` preserved) for both helpers.
48
+ - With color forced on, the dim `misc` ANSI escape wraps both `RUN:` and the `STDOUT:` header.
49
+ - All helpers stay silent when `debug` is unset.
50
+ - s01t21 file carries a supersede note pointing to s01t22.
51
+ - `uv run tox` green across all environments, including any pre-existing issues.
52
+
53
+ ## Open questions
54
+
55
+ - None outstanding — all grill questions resolved.
56
+
57
+ ## Out of scope
58
+
59
+ - No CONTEXT.md change — the "Debug diagnostic" glossary entry never pinned an idiom.
60
+ - No new ADR and no ADR 0002 edit — the idiom is cosmetic and trivially reversible (fails the "hard to reverse" bar), and ADR 0002 already describes these helpers as `misc`-styled escaped-arg wrappers without pinning a label style.
61
+ - Reworking `debug_output`'s empty-skip / escaping behavior — carried over unchanged from s01t21.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cli-error
3
- Version: 0.1.0
3
+ Version: 0.1.2
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,19 @@ 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.2
297
+ - Debug diagnostics now share a consistent uppercase `LABEL:` grammar — `debug_cmd` emits flush-left `RUN:`/`CWD:` lines, and `debug_output` couples a single `STDOUT:`/`STDERR:` header to its output block.
298
+
299
+ ### v0.1.1
300
+ - `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).
301
+ - 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.
302
+
303
+ ### v0.1.0
304
+ - `CliError` — structured exceptions with Rich-markup messages and a fluent builder for hints, detail blocks, and labelled context properties.
305
+ - `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.
306
+ - `CliExit` — a clean-exit signal that prints its message and exits with status 0.
307
+ - Theming via `make_console`, `make_theme`, and `DEFAULT_STYLES` — Rich consoles with default style roles, per-role overrides, `no_color` degradation, and stderr routing.
308
+ - 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.2"
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,46 +61,37 @@ 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."""
60
72
  if not self.debug:
61
73
  return
62
74
 
63
- self.debug_print("[misc]COMMAND: {cmd}[/misc]", cmd=shlex.join(cmd))
75
+ self.debug_print("[misc]RUN: {cmd}[/misc]", cmd=shlex.join(cmd))
64
76
  if cwd is not None:
65
- self.debug_print("[misc] cwd: {cwd}[/misc]", cwd=cwd)
77
+ self.debug_print("[misc]CWD: {cwd}[/misc]", cwd=cwd)
66
78
 
67
79
  def debug_output(self, stdout: str, stderr: str) -> None:
68
- """Emit captured subprocess output as debug diagnostics.
69
-
70
- Each non-empty stream is emitted as a header line followed by the
71
- captured text as one escaped ``misc`` block; empty streams are skipped.
72
- """
80
+ """Emit captured subprocess output as debug diagnostics."""
73
81
  if not self.debug:
74
82
  return
75
83
 
76
- for label, text in (("stdout", stdout), ("stderr", stderr)):
77
- trimmed = text.rstrip()
78
- if not trimmed:
79
- continue
84
+ trimmed = stdout.rstrip()
85
+ if trimmed:
86
+ self.debug_print("[misc]STDOUT:\n{text}[/misc]", text=trimmed)
80
87
 
81
- # TODO: this layout is ugly, lets prepend each line 'stdout: ' prefix
82
- self.debug_print("[misc] {label}:[/misc]", label=label)
83
- self.debug_print("[misc]{text}[/misc]", text=trimmed)
88
+ trimmed = stderr.rstrip()
89
+ if trimmed:
90
+ self.debug_print("[misc]STDERR:\n{text}[/misc]", text=trimmed)
84
91
 
85
92
  @contextmanager
86
93
  def handler(self) -> Generator[None]:
87
- """Report errors and translate them into process exit codes.
88
-
89
- * ``CliExit`` prints its message and exits 0;
90
- * any other ``Exception`` is rendered via ``print_error`` and exits 1,
91
- additionally emitting a full traceback to the ``stderr`` console when
92
- ``debug`` is set.
93
- * ``KeyboardInterrupt``, ``SystemExit`` and any other ``BaseException``
94
- propagate untouched.
95
- """
94
+ """Report errors and translate them into process exit codes."""
96
95
  try:
97
96
  yield
98
97
  except CliExit as ex:
@@ -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")