lib-cli-exit-tools 1.0.2__tar.gz → 1.0.3__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.
- {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/CHANGELOG.md +7 -3
- {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/CONTRIBUTING.md +1 -1
- {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/Makefile +3 -10
- {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/PKG-INFO +4 -7
- {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/README.md +3 -6
- {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/codecov.yml +1 -1
- {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/packaging/README.md +2 -2
- {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/packaging/brew/Formula/lib-cli-exit-tools.rb +1 -1
- {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/packaging/conda/recipe/meta.yaml +1 -1
- {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/packaging/nix/flake.nix +2 -2
- {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/pyproject.toml +1 -2
- lib_cli_exit_tools-1.0.3/qlty.toml +2 -0
- {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/scripts/_utils.py +2 -2
- {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/scripts/bump.py +1 -1
- {lib_cli_exit_tools-1.0.2/tools → lib_cli_exit_tools-1.0.3/scripts}/bump_version.py +1 -1
- lib_cli_exit_tools-1.0.2/scripts/menu_tui.py → lib_cli_exit_tools-1.0.3/scripts/menu.py +41 -58
- {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/scripts/test.py +24 -11
- lib_cli_exit_tools-1.0.2/docs/textual/README.md +0 -109
- lib_cli_exit_tools-1.0.2/docs/textual/snippets/actions_bindings.py +0 -24
- lib_cli_exit_tools-1.0.2/docs/textual/snippets/listview_basic.py +0 -20
- lib_cli_exit_tools-1.0.2/docs/textual/snippets/mount_await.py +0 -21
- lib_cli_exit_tools-1.0.2/docs/textual/snippets/richlog_stream.py +0 -56
- lib_cli_exit_tools-1.0.2/scripts/menu.py +0 -310
- lib_cli_exit_tools-1.0.2/scripts/menu_cli.py +0 -136
- {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/.env.example +0 -0
- {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/.github/workflows/ci.yml +0 -0
- {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/.github/workflows/codeql.yml +0 -0
- {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/.github/workflows/release.yml +0 -0
- {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/.gitignore +0 -0
- {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/AGENTS.md +0 -0
- {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/LICENSE +0 -0
- {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/notebooks/Quickstart.ipynb +0 -0
- {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/scripts/__init__.py +0 -0
- {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/scripts/build.py +0 -0
- {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/scripts/bump_major.py +0 -0
- {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/scripts/bump_minor.py +0 -0
- {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/scripts/bump_patch.py +0 -0
- {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/scripts/clean.py +0 -0
- {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/scripts/dev.py +0 -0
- {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/scripts/install.py +0 -0
- {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/scripts/push.py +0 -0
- {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/scripts/release.py +0 -0
- {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/scripts/run_cli.py +0 -0
- {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/scripts/version_current.py +0 -0
- {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/src/lib_cli_exit_tools/__init__.py +0 -0
- {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/src/lib_cli_exit_tools/__init__conf__.py +0 -0
- {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/src/lib_cli_exit_tools/__main__.py +0 -0
- {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/src/lib_cli_exit_tools/cli.py +0 -0
- {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/src/lib_cli_exit_tools/lib_cli_exit_tools.py +0 -0
- {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/src/lib_cli_exit_tools/py.typed +0 -0
- {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/tests/test_cli.py +0 -0
- {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/tests/test_cli_extra.py +0 -0
- {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/tests/test_exit_tools.py +0 -0
- {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/tests/test_lib_extra.py +0 -0
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.0.3] - 2025-09-16
|
|
4
|
+
|
|
5
|
+
- added make menu
|
|
6
|
+
|
|
3
7
|
## [1.0.2] - 2025-09-15
|
|
4
8
|
|
|
5
|
-
-
|
|
9
|
+
- minor internals
|
|
6
10
|
|
|
7
11
|
## [1.0.1] - 2025-09-15
|
|
8
12
|
|
|
9
|
-
-
|
|
13
|
+
- minor internals
|
|
10
14
|
|
|
11
15
|
## [1.0.0] - 2025-09-15
|
|
12
16
|
|
|
@@ -16,7 +20,7 @@
|
|
|
16
20
|
|
|
17
21
|
### Added
|
|
18
22
|
- CI job to execute Jupyter notebook on Python 3.10 (installs ipykernel, normalizes notebook IDs) and a tag‑time packaging‑consistency check.
|
|
19
|
-
- Packaging auto‑sync: `make test`, `make push`, and the bump script now align Conda/Brew/Nix files to `pyproject.toml` version, `requires-python`, and runtime deps. Homebrew resources are updated (URL + sha256) using PyPI metadata. A sync‑only mode is available via `python
|
|
23
|
+
- Packaging auto‑sync: `make test`, `make push`, and the bump script now align Conda/Brew/Nix files to `pyproject.toml` version, `requires-python`, and runtime deps. Homebrew resources are updated (URL + sha256) using PyPI metadata. A sync‑only mode is available via `python scripts/bump_version.py --sync-packaging`.
|
|
20
24
|
- Tests covering SystemExit variants, tolerant output printing, English CLI messages, and Windows mapping via monkeypatched `os.name`.
|
|
21
25
|
|
|
22
26
|
### Changed
|
|
@@ -28,7 +28,7 @@ Thanks for helping improve lib_cli_exit_tools! This guide keeps changes small, s
|
|
|
28
28
|
- Homebrew: ensures a `resource "<dep>"` for each dep; if pinned, uses that version; otherwise pins to the latest PyPI release, updating `url` + `sha256`.
|
|
29
29
|
- Nix: updates the package version, `pkgs.pythonXYZPackages`/`pkgs.pythonXYZ` to match the min Python, and `propagatedBuildInputs` to include all deps as `pypkgs.<name>`.
|
|
30
30
|
- Network note: Homebrew resource updates fetch PyPI metadata; if offline, those updates may be skipped.
|
|
31
|
-
- If packaging drifts, run: `python
|
|
31
|
+
- If packaging drifts, run: `python scripts/bump_version.py --sync-packaging`.
|
|
32
32
|
- CI enforces consistency on tags via a `packaging-consistency` job.
|
|
33
33
|
|
|
34
34
|
## 5) Tests & Style
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
SHELL := /bin/bash
|
|
2
2
|
.ONESHELL:
|
|
3
|
-
|
|
4
3
|
# Config
|
|
5
4
|
PY ?= python3
|
|
6
5
|
PIP ?= pip
|
|
@@ -18,7 +17,7 @@ FAIL_UNDER ?= 80
|
|
|
18
17
|
# - off : never run coverage
|
|
19
18
|
COVERAGE ?= on
|
|
20
19
|
|
|
21
|
-
.PHONY: help install dev test run clean build push release version-current bump bump-patch bump-minor bump-major menu
|
|
20
|
+
.PHONY: help install dev test run clean build push release version-current bump bump-patch bump-minor bump-major menu _bootstrap-dev
|
|
22
21
|
|
|
23
22
|
help: ## Show help
|
|
24
23
|
@grep -E '^[a-zA-Z_-]+:.*?## ' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
|
|
@@ -95,11 +94,5 @@ build: ## Build wheel/sdist and attempt conda, brew, and nix builds (auto-instal
|
|
|
95
94
|
release: ## Create and push tag vX.Y.Z from pyproject, create GitHub release (if gh present), then sync packaging
|
|
96
95
|
$(PY) scripts/release.py --remote=$(REMOTE)
|
|
97
96
|
|
|
98
|
-
menu: ## Interactive menu
|
|
99
|
-
$(PY) -u scripts/
|
|
100
|
-
|
|
101
|
-
menu-cli: ## Force simple CLI prompt menu (no TUI)
|
|
102
|
-
$(PY) -u scripts/menu_cli.py < /dev/tty > /dev/tty 2>&1
|
|
103
|
-
|
|
104
|
-
menu-tui: ## Force Textual TUI menu (may not work on limited terminals)
|
|
105
|
-
$(PY) -u scripts/menu_tui.py < /dev/tty > /dev/tty 2>&1
|
|
97
|
+
menu: ## Interactive TUI menu
|
|
98
|
+
$(PY) -u scripts/menu.py < /dev/tty > /dev/tty 2>&1
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lib_cli_exit_tools
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.3
|
|
4
4
|
Summary: CLI exit handling helpers: clean signals, exit codes, and error printing
|
|
5
5
|
Project-URL: Homepage, https://github.com/bitranox/lib_cli_exit_tools
|
|
6
6
|
Project-URL: Repository, https://github.com/bitranox/lib_cli_exit_tools.git
|
|
@@ -144,8 +144,6 @@ pip install .
|
|
|
144
144
|
| `push` | Commit all changes once and push to GitHub (no CI monitoring) |
|
|
145
145
|
| `build` | Build wheel/sdist and attempt conda, brew, and nix builds (auto-installs tools if missing) |
|
|
146
146
|
| `menu` | Interactive TUI to run targets and edit parameters (requires dev dep: textual) |
|
|
147
|
-
| `menu-cli` | Force simple prompt menu (no TUI) |
|
|
148
|
-
| `menu-tui` | Force Textual TUI (may not work in limited terminals) |
|
|
149
147
|
|
|
150
148
|
#### Target Parameters (env vars)
|
|
151
149
|
|
|
@@ -162,6 +160,7 @@ pip install .
|
|
|
162
160
|
- `test`
|
|
163
161
|
- `COVERAGE=on|auto|off` (default: `on`) — controls pytest coverage run and Codecov upload
|
|
164
162
|
- `SKIP_BOOTSTRAP=1` — skip auto-install of dev tools if missing
|
|
163
|
+
- `TEST_VERBOSE=1` — echo each command executed by the test harness
|
|
165
164
|
- Also respects `CODECOV_TOKEN` when needed for uploads
|
|
166
165
|
|
|
167
166
|
- `run`
|
|
@@ -195,7 +194,7 @@ pip install .
|
|
|
195
194
|
|
|
196
195
|
### Interactive Menu (Textual)
|
|
197
196
|
|
|
198
|
-
`make menu` launches a colorful terminal UI (powered by `textual`) to browse targets, edit parameters, and run them with live output.
|
|
197
|
+
`make menu` launches a colorful terminal UI (powered by `textual`) to browse targets, edit parameters, and run them with live output.
|
|
199
198
|
|
|
200
199
|
Install dev extras if you haven’t:
|
|
201
200
|
|
|
@@ -207,8 +206,6 @@ Run the menu:
|
|
|
207
206
|
|
|
208
207
|
```bash
|
|
209
208
|
make menu
|
|
210
|
-
make menu-cli # always use simple prompt menu
|
|
211
|
-
make menu-tui # force TUI (if your terminal supports it)
|
|
212
209
|
```
|
|
213
210
|
|
|
214
211
|
#### Target Details
|
|
@@ -288,7 +285,7 @@ COVERAGE=on make test # force coverage and generate coverage.xml/codecov.
|
|
|
288
285
|
- Homebrew: updates the source URL tag to `vX.Y.Z` and sets `depends_on "python@X.Y"` to match `requires-python`.
|
|
289
286
|
- Nix: updates the package `version`, example `rev = "vX.Y.Z"`, and switches `pkgs.pythonXYZPackages` / `pkgs.pythonXYZ` to match the minimum Python version from `requires-python`.
|
|
290
287
|
|
|
291
|
-
- To run just the sync without bumping versions: `python
|
|
288
|
+
- To run just the sync without bumping versions: `python scripts/bump_version.py --sync-packaging`.
|
|
292
289
|
|
|
293
290
|
- On release tags (`v*.*.*`), CI validates that packaging files are consistent with `pyproject.toml` and will fail if they drift.
|
|
294
291
|
|
|
@@ -114,8 +114,6 @@ pip install .
|
|
|
114
114
|
| `push` | Commit all changes once and push to GitHub (no CI monitoring) |
|
|
115
115
|
| `build` | Build wheel/sdist and attempt conda, brew, and nix builds (auto-installs tools if missing) |
|
|
116
116
|
| `menu` | Interactive TUI to run targets and edit parameters (requires dev dep: textual) |
|
|
117
|
-
| `menu-cli` | Force simple prompt menu (no TUI) |
|
|
118
|
-
| `menu-tui` | Force Textual TUI (may not work in limited terminals) |
|
|
119
117
|
|
|
120
118
|
#### Target Parameters (env vars)
|
|
121
119
|
|
|
@@ -132,6 +130,7 @@ pip install .
|
|
|
132
130
|
- `test`
|
|
133
131
|
- `COVERAGE=on|auto|off` (default: `on`) — controls pytest coverage run and Codecov upload
|
|
134
132
|
- `SKIP_BOOTSTRAP=1` — skip auto-install of dev tools if missing
|
|
133
|
+
- `TEST_VERBOSE=1` — echo each command executed by the test harness
|
|
135
134
|
- Also respects `CODECOV_TOKEN` when needed for uploads
|
|
136
135
|
|
|
137
136
|
- `run`
|
|
@@ -165,7 +164,7 @@ pip install .
|
|
|
165
164
|
|
|
166
165
|
### Interactive Menu (Textual)
|
|
167
166
|
|
|
168
|
-
`make menu` launches a colorful terminal UI (powered by `textual`) to browse targets, edit parameters, and run them with live output.
|
|
167
|
+
`make menu` launches a colorful terminal UI (powered by `textual`) to browse targets, edit parameters, and run them with live output.
|
|
169
168
|
|
|
170
169
|
Install dev extras if you haven’t:
|
|
171
170
|
|
|
@@ -177,8 +176,6 @@ Run the menu:
|
|
|
177
176
|
|
|
178
177
|
```bash
|
|
179
178
|
make menu
|
|
180
|
-
make menu-cli # always use simple prompt menu
|
|
181
|
-
make menu-tui # force TUI (if your terminal supports it)
|
|
182
179
|
```
|
|
183
180
|
|
|
184
181
|
#### Target Details
|
|
@@ -258,7 +255,7 @@ COVERAGE=on make test # force coverage and generate coverage.xml/codecov.
|
|
|
258
255
|
- Homebrew: updates the source URL tag to `vX.Y.Z` and sets `depends_on "python@X.Y"` to match `requires-python`.
|
|
259
256
|
- Nix: updates the package `version`, example `rev = "vX.Y.Z"`, and switches `pkgs.pythonXYZPackages` / `pkgs.pythonXYZ` to match the minimum Python version from `requires-python`.
|
|
260
257
|
|
|
261
|
-
- To run just the sync without bumping versions: `python
|
|
258
|
+
- To run just the sync without bumping versions: `python scripts/bump_version.py --sync-packaging`.
|
|
262
259
|
|
|
263
260
|
- On release tags (`v*.*.*`), CI validates that packaging files are consistent with `pyproject.toml` and will fail if they drift.
|
|
264
261
|
|
|
@@ -9,13 +9,13 @@ This directory contains packaging files and an automated sync flow so they stay
|
|
|
9
9
|
- Or semantic part: `make bump PART=patch|minor|major`
|
|
10
10
|
- To only synchronize packaging to the current `pyproject.toml` without changing versions:
|
|
11
11
|
```bash
|
|
12
|
-
python3
|
|
12
|
+
python3 scripts/bump_version.py --sync-packaging
|
|
13
13
|
```
|
|
14
14
|
- `make test` runs the same sync step up-front to keep packaging up to date.
|
|
15
15
|
|
|
16
16
|
## What auto-sync updates
|
|
17
17
|
|
|
18
|
-
The script `
|
|
18
|
+
The script `scripts/bump_version.py` updates the following when network access is available:
|
|
19
19
|
|
|
20
20
|
1) Conda recipe (`packaging/conda/recipe/meta.yaml`)
|
|
21
21
|
- Sets Jinja version to the current `pyproject.toml` version.
|
{lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/packaging/brew/Formula/lib-cli-exit-tools.rb
RENAMED
|
@@ -3,7 +3,7 @@ class LibCliExitTools < Formula
|
|
|
3
3
|
|
|
4
4
|
desc "CLI exit handling helpers: clean signals, exit codes, and error printing"
|
|
5
5
|
homepage "https://github.com/bitranox/lib_cli_exit_tools"
|
|
6
|
-
url "https://github.com/bitranox/lib_cli_exit_tools/archive/refs/tags/v1.0.
|
|
6
|
+
url "https://github.com/bitranox/lib_cli_exit_tools/archive/refs/tags/v1.0.3.tar.gz"
|
|
7
7
|
k3cc316e7d0b09dd79e617cb3e91b14ee414b7e6695b108a326d9d3ccd08dd9"
|
|
8
8
|
license "MIT"
|
|
9
9
|
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
{
|
|
36
36
|
packages.default = pypkgs.buildPythonPackage {
|
|
37
37
|
pname = "lib_cli_exit_tools";
|
|
38
|
-
version = "1.0.
|
|
38
|
+
version = "1.0.3";
|
|
39
39
|
pyproject = true;
|
|
40
40
|
# Build from the repository root (two levels up from packaging/nix)
|
|
41
41
|
src = ../..;
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
# src = pkgs.fetchFromGitHub {
|
|
44
44
|
# owner = "bitranox";
|
|
45
45
|
# repo = "lib_cli_exit_tools";
|
|
46
|
-
# rev = "v1.0.
|
|
46
|
+
# rev = "v1.0.3";
|
|
47
47
|
# sha256 = "<fill-me>";
|
|
48
48
|
# };
|
|
49
49
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "lib_cli_exit_tools"
|
|
3
|
-
version = "1.0.
|
|
3
|
+
version = "1.0.3"
|
|
4
4
|
description = "CLI exit handling helpers: clean signals, exit codes, and error printing"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.10"
|
|
@@ -62,7 +62,6 @@ target-version = "py313"
|
|
|
62
62
|
typeCheckingMode = "strict"
|
|
63
63
|
exclude = [
|
|
64
64
|
"scripts/menu.py",
|
|
65
|
-
"docs",
|
|
66
65
|
]
|
|
67
66
|
|
|
68
67
|
[tool.pytest.ini_options]
|
|
@@ -49,7 +49,7 @@ def cmd_exists(name: str) -> bool:
|
|
|
49
49
|
|
|
50
50
|
def read_version_from_pyproject(pyproject: Path = Path("pyproject.toml")) -> str:
|
|
51
51
|
try:
|
|
52
|
-
import tomllib # pyright: ignore[reportMissingImports]
|
|
52
|
+
import tomllib # pyright: ignore[reportMissingImports, reportMissingTypeStubs]
|
|
53
53
|
|
|
54
54
|
data = tomllib.loads(pyproject.read_text(encoding="utf-8"))
|
|
55
55
|
version = str(data.get("project", {}).get("version", ""))
|
|
@@ -108,7 +108,7 @@ def gh_release_edit(tag: str, title: str, body: str) -> None:
|
|
|
108
108
|
|
|
109
109
|
|
|
110
110
|
def sync_packaging() -> None:
|
|
111
|
-
run([sys.executable, "
|
|
111
|
+
run([sys.executable, "scripts/bump_version.py", "--sync-packaging"], check=False)
|
|
112
112
|
|
|
113
113
|
|
|
114
114
|
def bootstrap_dev() -> None:
|
|
@@ -14,7 +14,7 @@ from scripts._utils import run # noqa: E402
|
|
|
14
14
|
@click.option("--pyproject", type=click.Path(path_type=Path), default=Path("pyproject.toml"))
|
|
15
15
|
@click.option("--changelog", type=click.Path(path_type=Path), default=Path("CHANGELOG.md"))
|
|
16
16
|
def main(version_: str | None, part: str | None, pyproject: Path, changelog: Path) -> None:
|
|
17
|
-
args = [sys.executable, "
|
|
17
|
+
args = [sys.executable, "scripts/bump_version.py"]
|
|
18
18
|
if version_:
|
|
19
19
|
args += ["--version", version_]
|
|
20
20
|
else:
|
|
@@ -11,7 +11,7 @@ import hashlib
|
|
|
11
11
|
|
|
12
12
|
# Optional TOML parser for Python < 3.11 support during type checking
|
|
13
13
|
try: # pragma: no cover - import resolution
|
|
14
|
-
import tomllib as _tomllib # type: ignore
|
|
14
|
+
import tomllib as _tomllib # type: ignore
|
|
15
15
|
except Exception: # pragma: no cover - best-effort fallback
|
|
16
16
|
try:
|
|
17
17
|
import tomli as _tomllib # type: ignore
|
|
@@ -4,13 +4,13 @@ import asyncio
|
|
|
4
4
|
import os
|
|
5
5
|
import signal
|
|
6
6
|
from dataclasses import dataclass
|
|
7
|
-
from typing import Callable, Dict, Optional
|
|
7
|
+
from typing import TYPE_CHECKING, Callable, Dict, Optional
|
|
8
8
|
|
|
9
9
|
import contextlib
|
|
10
10
|
|
|
11
11
|
from rich.text import Text
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
14
|
from textual import events
|
|
15
15
|
from textual.app import App, ComposeResult
|
|
16
16
|
from textual.containers import Container, Horizontal, Vertical
|
|
@@ -27,13 +27,31 @@ try:
|
|
|
27
27
|
Select,
|
|
28
28
|
Static,
|
|
29
29
|
)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
else: # pragma: no cover
|
|
31
|
+
try:
|
|
32
|
+
from textual import events
|
|
33
|
+
from textual.app import App, ComposeResult
|
|
34
|
+
from textual.containers import Container, Horizontal, Vertical
|
|
35
|
+
from textual.reactive import reactive
|
|
36
|
+
from textual.screen import Screen
|
|
37
|
+
from textual.widgets import (
|
|
38
|
+
Button,
|
|
39
|
+
Footer,
|
|
40
|
+
Input,
|
|
41
|
+
Label,
|
|
42
|
+
ListItem,
|
|
43
|
+
ListView,
|
|
44
|
+
RichLog,
|
|
45
|
+
Select,
|
|
46
|
+
Static,
|
|
47
|
+
)
|
|
48
|
+
except Exception as exc: # pragma: no cover - textual import errors surface clearly
|
|
49
|
+
import sys
|
|
50
|
+
import traceback
|
|
33
51
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
52
|
+
print("[menu-tui] Failed to import Textual components:", file=sys.stderr)
|
|
53
|
+
traceback.print_exc()
|
|
54
|
+
raise SystemExit("textual is not installed or incompatible in this environment. Install dev extras: pip install -e .[dev]") from exc
|
|
37
55
|
|
|
38
56
|
|
|
39
57
|
@dataclass(slots=True)
|
|
@@ -84,6 +102,12 @@ def _build_targets() -> list[Target]:
|
|
|
84
102
|
default=_env_default("SKIP_BOOTSTRAP", "0"),
|
|
85
103
|
choices=["0", "1"],
|
|
86
104
|
),
|
|
105
|
+
Param(
|
|
106
|
+
"TEST_VERBOSE",
|
|
107
|
+
"Verbose test runner output (1=yes, 0=no)",
|
|
108
|
+
default=_env_default("TEST_VERBOSE", "0"),
|
|
109
|
+
choices=["0", "1"],
|
|
110
|
+
),
|
|
87
111
|
],
|
|
88
112
|
),
|
|
89
113
|
Target("run", "Run CLI (shows --help)", []),
|
|
@@ -327,7 +351,7 @@ class ParamScreen(Screen[Dict[str, str] | None]):
|
|
|
327
351
|
self._preset = preset
|
|
328
352
|
self._inputs: Dict[str, Input | Select[str]] = {}
|
|
329
353
|
self._error: Static | None = None
|
|
330
|
-
self.
|
|
354
|
+
self._ok_button: Button | None = None
|
|
331
355
|
|
|
332
356
|
def compose(self) -> ComposeResult: # type: ignore[override]
|
|
333
357
|
with Container(id="backdrop"):
|
|
@@ -354,38 +378,16 @@ class ParamScreen(Screen[Dict[str, str] | None]):
|
|
|
354
378
|
yield widget
|
|
355
379
|
with Horizontal(id="buttons"):
|
|
356
380
|
ok_btn = Button("OK", id="ok-btn")
|
|
357
|
-
|
|
358
|
-
self._buttons = [ok_btn, cancel_btn]
|
|
381
|
+
self._ok_button = ok_btn
|
|
359
382
|
yield ok_btn
|
|
360
|
-
yield cancel_btn
|
|
361
383
|
yield Footer()
|
|
362
384
|
|
|
363
385
|
def on_mount(self) -> None: # type: ignore[override]
|
|
364
|
-
self.
|
|
386
|
+
self._focus_ok_button()
|
|
365
387
|
|
|
366
388
|
def on_button_pressed(self, event: Button.Pressed) -> None: # type: ignore[override]
|
|
367
389
|
if event.button.id == "ok-btn":
|
|
368
390
|
self._submit()
|
|
369
|
-
elif event.button.id == "cancel-btn":
|
|
370
|
-
self.action_cancel()
|
|
371
|
-
|
|
372
|
-
def on_key(self, event: events.Key) -> None: # type: ignore[override]
|
|
373
|
-
if event.key not in {"left", "right"}:
|
|
374
|
-
return
|
|
375
|
-
focused = self.focused
|
|
376
|
-
if not isinstance(focused, Button):
|
|
377
|
-
return
|
|
378
|
-
try:
|
|
379
|
-
current = self._buttons.index(focused)
|
|
380
|
-
except ValueError:
|
|
381
|
-
return
|
|
382
|
-
if event.key == "left":
|
|
383
|
-
next_index = max(current - 1, 0)
|
|
384
|
-
else:
|
|
385
|
-
next_index = min(current + 1, len(self._buttons) - 1)
|
|
386
|
-
if next_index != current:
|
|
387
|
-
self._buttons[next_index].focus()
|
|
388
|
-
event.stop()
|
|
389
391
|
|
|
390
392
|
def on_input_submitted(self, event: Input.Submitted) -> None: # type: ignore[override]
|
|
391
393
|
if event.input.id and event.input.id.startswith("input-"):
|
|
@@ -399,6 +401,10 @@ class ParamScreen(Screen[Dict[str, str] | None]):
|
|
|
399
401
|
widget.focus()
|
|
400
402
|
return
|
|
401
403
|
|
|
404
|
+
def _focus_ok_button(self) -> None:
|
|
405
|
+
if self._ok_button is not None:
|
|
406
|
+
self._ok_button.focus()
|
|
407
|
+
|
|
402
408
|
def _submit(self) -> None:
|
|
403
409
|
values, error = self._gather_values()
|
|
404
410
|
if error:
|
|
@@ -450,8 +456,6 @@ class RunScreen(Screen[Optional[int]]):
|
|
|
450
456
|
self._log = RichLog(highlight=True, markup=True, id="log")
|
|
451
457
|
self._status = Static("", id="status")
|
|
452
458
|
self._ok_button = Button("OK", id="ok-btn", disabled=True)
|
|
453
|
-
self._cancel_button = Button("Cancel", id="cancel-btn")
|
|
454
|
-
self._buttons: list[Button] = [self._ok_button, self._cancel_button]
|
|
455
459
|
self._proc: asyncio.subprocess.Process | None = None
|
|
456
460
|
self._runner: asyncio.Task[None] | None = None
|
|
457
461
|
self._exit_code: Optional[int] = None
|
|
@@ -467,12 +471,11 @@ class RunScreen(Screen[Optional[int]]):
|
|
|
467
471
|
yield self._status
|
|
468
472
|
with Horizontal(id="buttons"):
|
|
469
473
|
yield self._ok_button
|
|
470
|
-
yield self._cancel_button
|
|
471
474
|
yield Footer()
|
|
472
475
|
|
|
473
476
|
async def on_mount(self) -> None: # type: ignore[override]
|
|
474
|
-
self._status.update("Running… Press
|
|
475
|
-
self.
|
|
477
|
+
self._status.update("Running… Press Esc to cancel.")
|
|
478
|
+
self._ok_button.focus()
|
|
476
479
|
self._runner = asyncio.create_task(self._run_process())
|
|
477
480
|
|
|
478
481
|
async def on_unmount(self) -> None: # type: ignore[override]
|
|
@@ -484,26 +487,6 @@ class RunScreen(Screen[Optional[int]]):
|
|
|
484
487
|
def on_button_pressed(self, event: Button.Pressed) -> None: # type: ignore[override]
|
|
485
488
|
if event.button.id == "ok-btn":
|
|
486
489
|
self.dismiss(self._exit_code)
|
|
487
|
-
elif event.button.id == "cancel-btn":
|
|
488
|
-
self.action_cancel()
|
|
489
|
-
|
|
490
|
-
def on_key(self, event: events.Key) -> None: # type: ignore[override]
|
|
491
|
-
if event.key not in {"left", "right"}:
|
|
492
|
-
return
|
|
493
|
-
focused = self.focused
|
|
494
|
-
if not isinstance(focused, Button):
|
|
495
|
-
return
|
|
496
|
-
try:
|
|
497
|
-
current = self._buttons.index(focused)
|
|
498
|
-
except ValueError:
|
|
499
|
-
return
|
|
500
|
-
if event.key == "left":
|
|
501
|
-
next_index = max(current - 1, 0)
|
|
502
|
-
else:
|
|
503
|
-
next_index = min(current + 1, len(self._buttons) - 1)
|
|
504
|
-
if next_index != current:
|
|
505
|
-
self._buttons[next_index].focus()
|
|
506
|
-
event.stop()
|
|
507
490
|
|
|
508
491
|
def action_cancel(self) -> None:
|
|
509
492
|
if self._proc and self._proc.returncode is None:
|
|
@@ -13,23 +13,38 @@ from scripts._utils import bootstrap_dev, cmd_exists, run, sync_packaging # noq
|
|
|
13
13
|
|
|
14
14
|
@click.command(help="Run lints, type-check, tests with coverage, and Codecov upload if configured")
|
|
15
15
|
@click.option("--coverage", type=click.Choice(["on", "auto", "off"]), default="on")
|
|
16
|
-
|
|
16
|
+
@click.option("--verbose", "-v", is_flag=True, help="Print executed commands before running them")
|
|
17
|
+
def main(coverage: str, verbose: bool) -> None:
|
|
18
|
+
env_verbose = os.getenv("TEST_VERBOSE", "").lower()
|
|
19
|
+
if not verbose and env_verbose in {"1", "true", "yes", "on"}:
|
|
20
|
+
verbose = True
|
|
21
|
+
|
|
22
|
+
def _run(cmd: list[str] | str, *, env: dict[str, str] | None = None, check: bool = True) -> None:
|
|
23
|
+
display = cmd if isinstance(cmd, str) else " ".join(cmd)
|
|
24
|
+
if verbose:
|
|
25
|
+
click.echo(f" $ {display}")
|
|
26
|
+
if env:
|
|
27
|
+
overrides = {k: v for k, v in env.items() if os.environ.get(k) != v}
|
|
28
|
+
if overrides:
|
|
29
|
+
env_view = " ".join(f"{k}={v}" for k, v in overrides.items())
|
|
30
|
+
click.echo(f" env {env_view}")
|
|
31
|
+
run(cmd, env=env, check=check) # type: ignore[arg-type]
|
|
32
|
+
|
|
17
33
|
bootstrap_dev()
|
|
18
34
|
|
|
19
35
|
click.echo("[0/4] Sync packaging (conda/brew/nix) with pyproject")
|
|
20
36
|
sync_packaging()
|
|
21
37
|
|
|
22
38
|
click.echo("[1/4] Ruff lint")
|
|
23
|
-
|
|
39
|
+
_run(["ruff", "check", "."], check=False) # type: ignore[list-item]
|
|
24
40
|
|
|
25
41
|
click.echo("[2/4] Ruff format (apply)")
|
|
26
|
-
|
|
42
|
+
_run(["ruff", "format", "."], check=False) # type: ignore[list-item]
|
|
27
43
|
|
|
28
44
|
click.echo("[3/4] Pyright type-check")
|
|
29
|
-
|
|
45
|
+
_run(["pyright"], check=False) # type: ignore[list-item]
|
|
30
46
|
|
|
31
47
|
click.echo("[4/4] Pytest with coverage")
|
|
32
|
-
# remove old coverage
|
|
33
48
|
for f in (".coverage", "coverage.xml"):
|
|
34
49
|
try:
|
|
35
50
|
Path(f).unlink()
|
|
@@ -42,7 +57,7 @@ def main(coverage: str) -> None:
|
|
|
42
57
|
cov_file = Path(tmp) / ".coverage"
|
|
43
58
|
click.echo(f"[coverage] file={cov_file}")
|
|
44
59
|
env = os.environ | {"COVERAGE_FILE": str(cov_file)}
|
|
45
|
-
|
|
60
|
+
_run(
|
|
46
61
|
[
|
|
47
62
|
"python",
|
|
48
63
|
"-m",
|
|
@@ -57,17 +72,15 @@ def main(coverage: str) -> None:
|
|
|
57
72
|
)
|
|
58
73
|
else:
|
|
59
74
|
click.echo("[coverage] disabled (set --coverage=on to force)")
|
|
60
|
-
|
|
75
|
+
_run(["python", "-m", "pytest", "-q"]) # type: ignore[list-item]
|
|
61
76
|
|
|
62
|
-
# Upload coverage to Codecov if present
|
|
63
77
|
if Path("coverage.xml").exists():
|
|
64
78
|
click.echo("Uploading coverage to Codecov")
|
|
65
79
|
if cmd_exists("codecov"):
|
|
66
80
|
version = run(["python", "-c", "import platform; print(platform.python_version())"]).out.strip()
|
|
67
|
-
|
|
81
|
+
_run(["codecov", "-f", "coverage.xml", "-F", "local", "-n", f"local-$(uname)-{version}"], check=False)
|
|
68
82
|
else:
|
|
69
|
-
|
|
70
|
-
run(
|
|
83
|
+
_run(
|
|
71
84
|
[
|
|
72
85
|
"bash",
|
|
73
86
|
"-lc",
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
(for this repo)
|
|
2
|
-
|
|
3
|
-
makThis is a pragmatic quick‑reference for building and debugging Textual UIs that match the versions we use here (Textual ≥ 6.x). It focuses on patterns we implemented in `scripts/menu_tui.py` (ListView + RichLog + async subprocess + Cancel).
|
|
4
|
-
|
|
5
|
-
## Install
|
|
6
|
-
|
|
7
|
-
- Dev extras (recommended):
|
|
8
|
-
- `pip install -e .[dev]`
|
|
9
|
-
- Or just Textual: `pip install textual`
|
|
10
|
-
- Verify: `python -c "import textual,sys; print(sys.version, textual.__version__)"`
|
|
11
|
-
|
|
12
|
-
## App + compose
|
|
13
|
-
|
|
14
|
-
```python
|
|
15
|
-
from textual.app import App, ComposeResult
|
|
16
|
-
from textual.widgets import Header, Footer, Static
|
|
17
|
-
|
|
18
|
-
class Hello(App):
|
|
19
|
-
def compose(self) -> ComposeResult:
|
|
20
|
-
yield Header()
|
|
21
|
-
yield Static("Hello Textual")
|
|
22
|
-
yield Footer()
|
|
23
|
-
|
|
24
|
-
if __name__ == "__main__":
|
|
25
|
-
Hello().run()
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
## Mounting: schedule vs await
|
|
29
|
-
|
|
30
|
-
- `self.mount(widget)` schedules. If you need to update/query the widget immediately, `await self.mount(widget)` inside an async handler (e.g. `async def on_mount(...)`).
|
|
31
|
-
|
|
32
|
-
```python
|
|
33
|
-
# See snippets/mount_await.py
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
## ListView (compose safely)
|
|
37
|
-
|
|
38
|
-
Create items inside `compose` or after mount. For initial items:
|
|
39
|
-
|
|
40
|
-
```python
|
|
41
|
-
from textual.widgets import ListView, ListItem, Label
|
|
42
|
-
|
|
43
|
-
yield ListView(
|
|
44
|
-
ListItem(Label("Item 1")),
|
|
45
|
-
ListItem(Label("Item 2")),
|
|
46
|
-
id="targets",
|
|
47
|
-
)
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
Adding later (after mount):
|
|
51
|
-
|
|
52
|
-
```python
|
|
53
|
-
lv = self.query_one("#targets", ListView)
|
|
54
|
-
await lv.append(ListItem(Label("New Item"))) # or lv.append(...) if in compose tick
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
## RichLog (Textual 6.x)
|
|
58
|
-
|
|
59
|
-
- Use `RichLog` (not `TextLog`).
|
|
60
|
-
- Append with `write()`.
|
|
61
|
-
|
|
62
|
-
```python
|
|
63
|
-
from textual.widgets import RichLog
|
|
64
|
-
log = RichLog(highlight=True)
|
|
65
|
-
log.write("Starting…")
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
## Async subprocess + live streaming
|
|
69
|
-
|
|
70
|
-
```python
|
|
71
|
-
# See snippets/richlog_stream.py
|
|
72
|
-
# gist: create_subprocess_exec + read stdout/stderr per line → log.write()
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
POSIX Cancel: `proc.send_signal(signal.SIGINT)`; Windows: `proc.terminate()`.
|
|
76
|
-
|
|
77
|
-
## Actions & key bindings
|
|
78
|
-
|
|
79
|
-
Bind keys to actions:
|
|
80
|
-
|
|
81
|
-
```python
|
|
82
|
-
# See snippets/actions_bindings.py
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
## CSS basics (inline)
|
|
86
|
-
|
|
87
|
-
```python
|
|
88
|
-
CSS = """
|
|
89
|
-
Screen { layout: horizontal; }
|
|
90
|
-
#right { width: 1fr; }
|
|
91
|
-
RichLog { border: tall $accent; }
|
|
92
|
-
"""
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
## Run the examples
|
|
96
|
-
|
|
97
|
-
```bash
|
|
98
|
-
python docs/textual/snippets/listview_basic.py
|
|
99
|
-
python docs/textual/snippets/richlog_stream.py
|
|
100
|
-
python docs/textual/snippets/mount_await.py
|
|
101
|
-
python docs/textual/snippets/actions_bindings.py
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
## Troubleshooting
|
|
105
|
-
|
|
106
|
-
- Nothing shows under `make`: ensure stdin/stdout is a TTY. We wire `/dev/tty` in Make.
|
|
107
|
-
- Textual import errors: check version (`textual.__version__`), install `pip install -e .[dev]`.
|
|
108
|
-
- MountError on ListView: don’t append before the ListView is mounted; yield items in `compose` or `await mount()` first.
|
|
109
|
-
|