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.
- {cli_error-0.1.0 → cli_error-0.1.2}/.github/workflows/ci.yml +2 -2
- {cli_error-0.1.0 → cli_error-0.1.2}/.github/workflows/release.yml +2 -2
- {cli_error-0.1.0 → cli_error-0.1.2}/.sandbox/README.md +0 -4
- {cli_error-0.1.0 → cli_error-0.1.2}/.sandbox/docker-compose.yml +0 -1
- {cli_error-0.1.0 → cli_error-0.1.2}/.tasker/s01-extract-repo-skills-error-reporting/README.md +7 -5
- {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
- {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
- {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
- {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
- cli_error-0.1.2/.tasker/s01-extract-repo-skills-error-reporting/s01t21-reconsider-debug-output-layout-evaluate.md +33 -0
- cli_error-0.1.2/.tasker/s01-extract-repo-skills-error-reporting/s01t22-unify-debug-helper-label-idiom.md +61 -0
- {cli_error-0.1.0 → cli_error-0.1.2}/PKG-INFO +1 -1
- {cli_error-0.1.0 → cli_error-0.1.2}/README.md +27 -6
- {cli_error-0.1.0 → cli_error-0.1.2}/pyproject.toml +1 -1
- {cli_error-0.1.0 → cli_error-0.1.2}/src/cli_error/_console.py +11 -0
- {cli_error-0.1.0 → cli_error-0.1.2}/src/cli_error/_reporter.py +28 -29
- cli_error-0.1.2/tests/helpers.py +33 -0
- {cli_error-0.1.0 → cli_error-0.1.2}/tests/test_console.py +13 -39
- {cli_error-0.1.0 → cli_error-0.1.2}/tests/test_errors.py +2 -3
- {cli_error-0.1.0 → cli_error-0.1.2}/tests/test_render.py +4 -19
- {cli_error-0.1.0 → cli_error-0.1.2}/tests/test_reporter.py +208 -11
- {cli_error-0.1.0 → cli_error-0.1.2}/uv.lock +68 -67
- cli_error-0.1.0/.claude/scheduled_tasks.lock +0 -1
- cli_error-0.1.0/.tasker/.closed +0 -16
- cli_error-0.1.0/.tasker/.recent +0 -1
- {cli_error-0.1.0 → cli_error-0.1.2}/.flake8 +0 -0
- {cli_error-0.1.0 → cli_error-0.1.2}/.gitignore +0 -0
- {cli_error-0.1.0 → cli_error-0.1.2}/.mcp.json +0 -0
- {cli_error-0.1.0 → cli_error-0.1.2}/.sandbox/.env.example +0 -0
- {cli_error-0.1.0 → cli_error-0.1.2}/.sandbox/base/Dockerfile +0 -0
- {cli_error-0.1.0 → cli_error-0.1.2}/.sandbox/base/tmux.conf +0 -0
- {cli_error-0.1.0 → cli_error-0.1.2}/.sandbox/base/zshrc +0 -0
- {cli_error-0.1.0 → cli_error-0.1.2}/.sandbox/run.sh +0 -0
- {cli_error-0.1.0 → cli_error-0.1.2}/.sandbox/scripts/_lib.sh +0 -0
- {cli_error-0.1.0 → cli_error-0.1.2}/.sandbox/scripts/attach.sh +0 -0
- {cli_error-0.1.0 → cli_error-0.1.2}/.sandbox/scripts/build.sh +0 -0
- {cli_error-0.1.0 → cli_error-0.1.2}/.sandbox/scripts/claude.sh +0 -0
- {cli_error-0.1.0 → cli_error-0.1.2}/.sandbox/scripts/shell.sh +0 -0
- {cli_error-0.1.0 → cli_error-0.1.2}/.sandbox/variants/python/Dockerfile +0 -0
- {cli_error-0.1.0 → cli_error-0.1.2}/.sandbox/variants/python/compose.override.yml +0 -0
- {cli_error-0.1.0 → cli_error-0.1.2}/.sandbox/worktree_run.sh +0 -0
- {cli_error-0.1.0 → cli_error-0.1.2}/.sandbox/worktree_shell.sh +0 -0
- {cli_error-0.1.0 → cli_error-0.1.2}/.tasker/.gitignore +0 -0
- {cli_error-0.1.0 → cli_error-0.1.2}/.tasker/archive/.gitkeep +0 -0
- {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
- {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
- {cli_error-0.1.0 → cli_error-0.1.2}/.tasker/s01-extract-repo-skills-error-reporting/s01t03-render-formatted-error.md +0 -0
- {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
- {cli_error-0.1.0 → cli_error-0.1.2}/.tasker/s01-extract-repo-skills-error-reporting/s01t05-cause-chain-rendering.md +0 -0
- {cli_error-0.1.0 → cli_error-0.1.2}/.tasker/s01-extract-repo-skills-error-reporting/s01t06-debug-traceback.md +0 -0
- {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
- {cli_error-0.1.0 → cli_error-0.1.2}/.tasker/s01-extract-repo-skills-error-reporting/s01t08-library-documentation-readme.md +0 -0
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {cli_error-0.1.0 → cli_error-0.1.2}/CLAUDE.md +0 -0
- {cli_error-0.1.0 → cli_error-0.1.2}/CONTEXT.md +0 -0
- {cli_error-0.1.0 → cli_error-0.1.2}/docs/adr/0001-error-formatting-and-theming.md +0 -0
- {cli_error-0.1.0 → cli_error-0.1.2}/docs/adr/0002-cli-reporter-output-facade.md +0 -0
- {cli_error-0.1.0 → cli_error-0.1.2}/docs/agents/dev-loop.md +0 -0
- {cli_error-0.1.0 → cli_error-0.1.2}/docs/agents/task-tracker.md +0 -0
- {cli_error-0.1.0 → cli_error-0.1.2}/src/cli_error/__init__.py +0 -0
- {cli_error-0.1.0 → cli_error-0.1.2}/src/cli_error/_errors.py +0 -0
- {cli_error-0.1.0 → cli_error-0.1.2}/src/cli_error/_render.py +0 -0
- {cli_error-0.1.0 → cli_error-0.1.2}/src/cli_error/py.typed +0 -0
- {cli_error-0.1.0 → cli_error-0.1.2}/tests/__init__.py +0 -0
- {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@
|
|
23
|
+
- uses: actions/checkout@v5
|
|
24
24
|
|
|
25
25
|
- name: Set up uv
|
|
26
|
-
uses: astral-sh/setup-uv@
|
|
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@
|
|
16
|
+
- uses: actions/checkout@v5
|
|
17
17
|
|
|
18
18
|
- name: Set up uv
|
|
19
|
-
uses: astral-sh/setup-uv@
|
|
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
|
{cli_error-0.1.0 → cli_error-0.1.2}/.tasker/s01-extract-repo-skills-error-reporting/README.md
RENAMED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
id: s01
|
|
3
3
|
slug: extract-repo-skills-error-reporting
|
|
4
|
-
status:
|
|
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
|
-
- [
|
|
57
|
-
- [
|
|
58
|
-
- [
|
|
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
|
-
- [
|
|
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)
|
|
@@ -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.
|
|
@@ -198,10 +198,12 @@ if __name__ == "__main__":
|
|
|
198
198
|
main()
|
|
199
199
|
```
|
|
200
200
|
|
|
201
|
-
`CliReporter(console, *, debug=False, console_err=None)` — when
|
|
202
|
-
omitted a stderr console is built for you
|
|
203
|
-
|
|
204
|
-
|
|
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()`
|
|
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.
|
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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]
|
|
75
|
+
self.debug_print("[misc]RUN: {cmd}[/misc]", cmd=shlex.join(cmd))
|
|
64
76
|
if cwd is not None:
|
|
65
|
-
self.debug_print("[misc]
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
continue
|
|
84
|
+
trimmed = stdout.rstrip()
|
|
85
|
+
if trimmed:
|
|
86
|
+
self.debug_print("[misc]STDOUT:\n{text}[/misc]", text=trimmed)
|
|
80
87
|
|
|
81
|
-
|
|
82
|
-
|
|
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 =
|
|
41
|
-
out =
|
|
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 =
|
|
83
|
-
assert
|
|
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 =
|
|
90
|
-
plain =
|
|
63
|
+
coloured = forced_terminal(make_theme(), no_color=False)
|
|
64
|
+
plain = forced_terminal(make_theme(), no_color=True)
|
|
91
65
|
|
|
92
|
-
coloured_out =
|
|
93
|
-
plain_out =
|
|
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 =
|
|
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 =
|
|
129
|
-
out =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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")
|