lib-cli-exit-tools 1.0.2__tar.gz → 1.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.1.0}/CHANGELOG.md +11 -3
- {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.1.0}/CONTRIBUTING.md +1 -1
- {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.1.0}/Makefile +3 -10
- {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.1.0}/PKG-INFO +36 -7
- {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.1.0}/README.md +34 -6
- {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.1.0}/codecov.yml +1 -1
- {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.1.0}/packaging/README.md +2 -2
- {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.1.0}/packaging/brew/Formula/lib-cli-exit-tools.rb +1 -1
- {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.1.0}/packaging/conda/recipe/meta.yaml +1 -1
- {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.1.0}/packaging/nix/flake.nix +2 -2
- {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.1.0}/pyproject.toml +2 -2
- lib_cli_exit_tools-1.1.0/qlty.toml +2 -0
- lib_cli_exit_tools-1.1.0/scripts/_utils.py +255 -0
- {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.1.0}/scripts/build.py +4 -2
- {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.1.0}/scripts/bump.py +1 -1
- {lib_cli_exit_tools-1.0.2/tools → lib_cli_exit_tools-1.1.0/scripts}/bump_version.py +39 -30
- lib_cli_exit_tools-1.0.2/scripts/menu_tui.py → lib_cli_exit_tools-1.1.0/scripts/menu.py +50 -59
- lib_cli_exit_tools-1.1.0/scripts/run_cli.py +26 -0
- lib_cli_exit_tools-1.1.0/scripts/test.py +129 -0
- lib_cli_exit_tools-1.1.0/src/lib_cli_exit_tools/__init__.py +33 -0
- lib_cli_exit_tools-1.1.0/src/lib_cli_exit_tools/__main__.py +4 -0
- lib_cli_exit_tools-1.1.0/src/lib_cli_exit_tools/cli.py +45 -0
- lib_cli_exit_tools-1.1.0/src/lib_cli_exit_tools/lib_cli_exit_tools.py +372 -0
- lib_cli_exit_tools-1.1.0/tests/test_cli.py +85 -0
- lib_cli_exit_tools-1.1.0/tests/test_cli_extra.py +32 -0
- lib_cli_exit_tools-1.1.0/tests/test_extend_cli.py +111 -0
- 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/_utils.py +0 -125
- 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/scripts/run_cli.py +0 -17
- lib_cli_exit_tools-1.0.2/scripts/test.py +0 -83
- lib_cli_exit_tools-1.0.2/src/lib_cli_exit_tools/__init__.py +0 -15
- lib_cli_exit_tools-1.0.2/src/lib_cli_exit_tools/__main__.py +0 -4
- lib_cli_exit_tools-1.0.2/src/lib_cli_exit_tools/cli.py +0 -138
- lib_cli_exit_tools-1.0.2/src/lib_cli_exit_tools/lib_cli_exit_tools.py +0 -353
- lib_cli_exit_tools-1.0.2/tests/test_cli.py +0 -49
- lib_cli_exit_tools-1.0.2/tests/test_cli_extra.py +0 -54
- {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.1.0}/.env.example +0 -0
- {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.1.0}/.github/workflows/ci.yml +0 -0
- {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.1.0}/.github/workflows/codeql.yml +0 -0
- {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.1.0}/.github/workflows/release.yml +0 -0
- {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.1.0}/.gitignore +0 -0
- {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.1.0}/AGENTS.md +0 -0
- {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.1.0}/LICENSE +0 -0
- {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.1.0}/notebooks/Quickstart.ipynb +0 -0
- {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.1.0}/scripts/__init__.py +0 -0
- {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.1.0}/scripts/bump_major.py +0 -0
- {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.1.0}/scripts/bump_minor.py +0 -0
- {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.1.0}/scripts/bump_patch.py +0 -0
- {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.1.0}/scripts/clean.py +0 -0
- {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.1.0}/scripts/dev.py +0 -0
- {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.1.0}/scripts/install.py +0 -0
- {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.1.0}/scripts/push.py +0 -0
- {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.1.0}/scripts/release.py +0 -0
- {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.1.0}/scripts/version_current.py +0 -0
- {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.1.0}/src/lib_cli_exit_tools/__init__conf__.py +0 -0
- {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.1.0}/src/lib_cli_exit_tools/py.typed +0 -0
- {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.1.0}/tests/test_exit_tools.py +0 -0
- {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.1.0}/tests/test_lib_extra.py +0 -0
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.1.0] - 2025-09-16
|
|
4
|
+
|
|
5
|
+
- introduced lib_cli_exit_tools.run_cli helper to reduce boilerplate code. see cli.py as example
|
|
6
|
+
|
|
7
|
+
## [1.0.3] - 2025-09-16
|
|
8
|
+
|
|
9
|
+
- added make menu
|
|
10
|
+
|
|
3
11
|
## [1.0.2] - 2025-09-15
|
|
4
12
|
|
|
5
|
-
-
|
|
13
|
+
- minor internals
|
|
6
14
|
|
|
7
15
|
## [1.0.1] - 2025-09-15
|
|
8
16
|
|
|
9
|
-
-
|
|
17
|
+
- minor internals
|
|
10
18
|
|
|
11
19
|
## [1.0.0] - 2025-09-15
|
|
12
20
|
|
|
@@ -16,7 +24,7 @@
|
|
|
16
24
|
|
|
17
25
|
### Added
|
|
18
26
|
- 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
|
|
27
|
+
- 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
28
|
- Tests covering SystemExit variants, tolerant output printing, English CLI messages, and Windows mapping via monkeypatched `os.name`.
|
|
21
29
|
|
|
22
30
|
### 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.1.0
|
|
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
|
|
@@ -25,6 +25,7 @@ Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
|
25
25
|
Requires-Dist: pytest>=8.2; extra == 'dev'
|
|
26
26
|
Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
27
27
|
Requires-Dist: textual>=0.50; extra == 'dev'
|
|
28
|
+
Requires-Dist: tomli>=2.0; extra == 'dev'
|
|
28
29
|
Requires-Dist: twine>=5.1; extra == 'dev'
|
|
29
30
|
Description-Content-Type: text/markdown
|
|
30
31
|
|
|
@@ -144,8 +145,6 @@ pip install .
|
|
|
144
145
|
| `push` | Commit all changes once and push to GitHub (no CI monitoring) |
|
|
145
146
|
| `build` | Build wheel/sdist and attempt conda, brew, and nix builds (auto-installs tools if missing) |
|
|
146
147
|
| `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
148
|
|
|
150
149
|
#### Target Parameters (env vars)
|
|
151
150
|
|
|
@@ -162,6 +161,7 @@ pip install .
|
|
|
162
161
|
- `test`
|
|
163
162
|
- `COVERAGE=on|auto|off` (default: `on`) — controls pytest coverage run and Codecov upload
|
|
164
163
|
- `SKIP_BOOTSTRAP=1` — skip auto-install of dev tools if missing
|
|
164
|
+
- `TEST_VERBOSE=1` — echo each command executed by the test harness
|
|
165
165
|
- Also respects `CODECOV_TOKEN` when needed for uploads
|
|
166
166
|
|
|
167
167
|
- `run`
|
|
@@ -195,7 +195,7 @@ pip install .
|
|
|
195
195
|
|
|
196
196
|
### Interactive Menu (Textual)
|
|
197
197
|
|
|
198
|
-
`make menu` launches a colorful terminal UI (powered by `textual`) to browse targets, edit parameters, and run them with live output.
|
|
198
|
+
`make menu` launches a colorful terminal UI (powered by `textual`) to browse targets, edit parameters, and run them with live output.
|
|
199
199
|
|
|
200
200
|
Install dev extras if you haven’t:
|
|
201
201
|
|
|
@@ -207,8 +207,6 @@ Run the menu:
|
|
|
207
207
|
|
|
208
208
|
```bash
|
|
209
209
|
make menu
|
|
210
|
-
make menu-cli # always use simple prompt menu
|
|
211
|
-
make menu-tui # force TUI (if your terminal supports it)
|
|
212
210
|
```
|
|
213
211
|
|
|
214
212
|
#### Target Details
|
|
@@ -233,6 +231,37 @@ cli-exit-tools --help # alias
|
|
|
233
231
|
lib_cli_exit_tools info
|
|
234
232
|
```
|
|
235
233
|
|
|
234
|
+
### Embed in your own CLI
|
|
235
|
+
|
|
236
|
+
The `run_cli` helper wraps any Click command with lib_cli_exit_tools’ signal and error handling.
|
|
237
|
+
|
|
238
|
+
```python
|
|
239
|
+
from __future__ import annotations
|
|
240
|
+
|
|
241
|
+
import click
|
|
242
|
+
|
|
243
|
+
from lib_cli_exit_tools import run_cli
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
@click.command()
|
|
247
|
+
def hello() -> None:
|
|
248
|
+
"""Minimal Click command with automatic signal-aware exit handling."""
|
|
249
|
+
|
|
250
|
+
click.echo("Hello from lib_cli_exit_tools!")
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
if __name__ == "__main__":
|
|
254
|
+
# run_cli handles SIGINT/SIGTERM/SIGBREAK, Click exceptions, and converts
|
|
255
|
+
# any other exception into an appropriate exit code.
|
|
256
|
+
raise SystemExit(run_cli(hello))
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Run it with:
|
|
260
|
+
|
|
261
|
+
```bash
|
|
262
|
+
python hello.py
|
|
263
|
+
```
|
|
264
|
+
|
|
236
265
|
Library:
|
|
237
266
|
|
|
238
267
|
```python
|
|
@@ -288,7 +317,7 @@ COVERAGE=on make test # force coverage and generate coverage.xml/codecov.
|
|
|
288
317
|
- Homebrew: updates the source URL tag to `vX.Y.Z` and sets `depends_on "python@X.Y"` to match `requires-python`.
|
|
289
318
|
- 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
319
|
|
|
291
|
-
- To run just the sync without bumping versions: `python
|
|
320
|
+
- To run just the sync without bumping versions: `python scripts/bump_version.py --sync-packaging`.
|
|
292
321
|
|
|
293
322
|
- On release tags (`v*.*.*`), CI validates that packaging files are consistent with `pyproject.toml` and will fail if they drift.
|
|
294
323
|
|
|
@@ -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
|
|
@@ -203,6 +200,37 @@ cli-exit-tools --help # alias
|
|
|
203
200
|
lib_cli_exit_tools info
|
|
204
201
|
```
|
|
205
202
|
|
|
203
|
+
### Embed in your own CLI
|
|
204
|
+
|
|
205
|
+
The `run_cli` helper wraps any Click command with lib_cli_exit_tools’ signal and error handling.
|
|
206
|
+
|
|
207
|
+
```python
|
|
208
|
+
from __future__ import annotations
|
|
209
|
+
|
|
210
|
+
import click
|
|
211
|
+
|
|
212
|
+
from lib_cli_exit_tools import run_cli
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
@click.command()
|
|
216
|
+
def hello() -> None:
|
|
217
|
+
"""Minimal Click command with automatic signal-aware exit handling."""
|
|
218
|
+
|
|
219
|
+
click.echo("Hello from lib_cli_exit_tools!")
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
if __name__ == "__main__":
|
|
223
|
+
# run_cli handles SIGINT/SIGTERM/SIGBREAK, Click exceptions, and converts
|
|
224
|
+
# any other exception into an appropriate exit code.
|
|
225
|
+
raise SystemExit(run_cli(hello))
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Run it with:
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
python hello.py
|
|
232
|
+
```
|
|
233
|
+
|
|
206
234
|
Library:
|
|
207
235
|
|
|
208
236
|
```python
|
|
@@ -258,7 +286,7 @@ COVERAGE=on make test # force coverage and generate coverage.xml/codecov.
|
|
|
258
286
|
- Homebrew: updates the source URL tag to `vX.Y.Z` and sets `depends_on "python@X.Y"` to match `requires-python`.
|
|
259
287
|
- 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
288
|
|
|
261
|
-
- To run just the sync without bumping versions: `python
|
|
289
|
+
- To run just the sync without bumping versions: `python scripts/bump_version.py --sync-packaging`.
|
|
262
290
|
|
|
263
291
|
- On release tags (`v*.*.*`), CI validates that packaging files are consistent with `pyproject.toml` and will fail if they drift.
|
|
264
292
|
|
|
@@ -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.1.0}/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.1.0.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.1.0";
|
|
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.1.0";
|
|
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.1.0"
|
|
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"
|
|
@@ -32,6 +32,7 @@ dev = [
|
|
|
32
32
|
"twine>=5.1",
|
|
33
33
|
"codecov>=2.1.13",
|
|
34
34
|
"textual>=0.50",
|
|
35
|
+
"tomli>=2.0",
|
|
35
36
|
]
|
|
36
37
|
|
|
37
38
|
[project.scripts]
|
|
@@ -62,7 +63,6 @@ target-version = "py313"
|
|
|
62
63
|
typeCheckingMode = "strict"
|
|
63
64
|
exclude = [
|
|
64
65
|
"scripts/menu.py",
|
|
65
|
-
"docs",
|
|
66
66
|
]
|
|
67
67
|
|
|
68
68
|
[tool.pytest.ini_options]
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
import shlex
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from subprocess import CompletedProcess
|
|
10
|
+
from typing import Any, Mapping, Sequence
|
|
11
|
+
from urllib.parse import urlparse
|
|
12
|
+
|
|
13
|
+
try: # Python 3.11+
|
|
14
|
+
import tomllib # type: ignore[attr-defined]
|
|
15
|
+
except ModuleNotFoundError: # pragma: no cover - fallback for older interpreters
|
|
16
|
+
try:
|
|
17
|
+
import tomli as tomllib # type: ignore
|
|
18
|
+
except ModuleNotFoundError: # pragma: no cover - pyproject parsing will degrade gracefully
|
|
19
|
+
tomllib = None # type: ignore
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(slots=True)
|
|
23
|
+
class RunResult:
|
|
24
|
+
code: int
|
|
25
|
+
out: str
|
|
26
|
+
err: str
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(slots=True)
|
|
30
|
+
class ProjectMetadata:
|
|
31
|
+
name: str
|
|
32
|
+
slug: str
|
|
33
|
+
repo_url: str
|
|
34
|
+
repo_host: str
|
|
35
|
+
repo_owner: str
|
|
36
|
+
repo_name: str
|
|
37
|
+
homepage: str
|
|
38
|
+
import_package: str
|
|
39
|
+
coverage_source: str
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def brew_formula_path(self) -> str:
|
|
43
|
+
return f"packaging/brew/Formula/{self.slug}.rb"
|
|
44
|
+
|
|
45
|
+
def github_tarball_url(self, version: str) -> str:
|
|
46
|
+
if self.repo_host == "github.com" and self.repo_owner and self.repo_name:
|
|
47
|
+
return f"https://github.com/{self.repo_owner}/{self.repo_name}/archive/refs/tags/v{version}.tar.gz"
|
|
48
|
+
return ""
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
_PYPROJECT_DATA_CACHE: dict[Path, dict[str, Any]] = {}
|
|
52
|
+
_METADATA_CACHE: dict[Path, ProjectMetadata] = {}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def run(
|
|
56
|
+
cmd: Sequence[str] | str,
|
|
57
|
+
*,
|
|
58
|
+
check: bool = True,
|
|
59
|
+
capture: bool = True,
|
|
60
|
+
cwd: str | None = None,
|
|
61
|
+
env: Mapping[str, str] | None = None,
|
|
62
|
+
dry_run: bool = False,
|
|
63
|
+
) -> RunResult:
|
|
64
|
+
if isinstance(cmd, str):
|
|
65
|
+
display = cmd
|
|
66
|
+
shell = True
|
|
67
|
+
args: Sequence[str] | str = cmd
|
|
68
|
+
else:
|
|
69
|
+
display = " ".join(shlex.quote(p) for p in cmd)
|
|
70
|
+
shell = False
|
|
71
|
+
args = list(cmd)
|
|
72
|
+
if dry_run:
|
|
73
|
+
print(f"[dry-run] {display}")
|
|
74
|
+
return RunResult(0, "", "")
|
|
75
|
+
proc: CompletedProcess[str] = subprocess.run(
|
|
76
|
+
args,
|
|
77
|
+
shell=shell,
|
|
78
|
+
cwd=cwd,
|
|
79
|
+
env=env,
|
|
80
|
+
text=True,
|
|
81
|
+
capture_output=capture,
|
|
82
|
+
)
|
|
83
|
+
if check and proc.returncode != 0:
|
|
84
|
+
raise SystemExit(proc.returncode)
|
|
85
|
+
return RunResult(int(proc.returncode or 0), proc.stdout or "", proc.stderr or "")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def cmd_exists(name: str) -> bool:
|
|
89
|
+
return subprocess.call(["bash", "-lc", f"command -v {shlex.quote(name)} >/dev/null 2>&1"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) == 0
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _normalize_slug(value: str) -> str:
|
|
93
|
+
slug = re.sub(r"[^A-Za-z0-9]+", "-", value).strip("-").lower()
|
|
94
|
+
return slug or value.replace("_", "-").lower()
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _load_pyproject(pyproject: Path) -> dict[str, Any]:
|
|
98
|
+
path = pyproject.resolve()
|
|
99
|
+
cached = _PYPROJECT_DATA_CACHE.get(path)
|
|
100
|
+
if cached is not None:
|
|
101
|
+
return cached
|
|
102
|
+
text = path.read_text(encoding="utf-8")
|
|
103
|
+
data: dict[str, Any] = {}
|
|
104
|
+
if tomllib is not None:
|
|
105
|
+
try:
|
|
106
|
+
data = tomllib.loads(text)
|
|
107
|
+
except Exception:
|
|
108
|
+
data = {}
|
|
109
|
+
_PYPROJECT_DATA_CACHE[path] = data
|
|
110
|
+
return data
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _derive_import_package(data: dict[str, Any], fallback: str) -> str:
|
|
114
|
+
try:
|
|
115
|
+
packages = data.get("tool", {}).get("hatch", {}).get("build", {}).get("targets", {}).get("wheel", {}).get("packages", [])
|
|
116
|
+
if isinstance(packages, list) and packages:
|
|
117
|
+
first = packages[0]
|
|
118
|
+
if isinstance(first, str) and first:
|
|
119
|
+
return Path(first).name
|
|
120
|
+
except AttributeError:
|
|
121
|
+
pass
|
|
122
|
+
project_scripts = data.get("project", {}).get("scripts", {})
|
|
123
|
+
if isinstance(project_scripts, dict):
|
|
124
|
+
for value in project_scripts.values():
|
|
125
|
+
if isinstance(value, str) and ":" in value:
|
|
126
|
+
module = value.split(":", 1)[0]
|
|
127
|
+
return module.split(".", 1)[0]
|
|
128
|
+
return fallback.replace("-", "_")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _derive_coverage_source(data: dict[str, Any], fallback: str) -> str:
|
|
132
|
+
try:
|
|
133
|
+
sources = data.get("tool", {}).get("coverage", {}).get("run", {}).get("source", [])
|
|
134
|
+
if isinstance(sources, list) and sources:
|
|
135
|
+
first = sources[0]
|
|
136
|
+
if isinstance(first, str) and first:
|
|
137
|
+
return first
|
|
138
|
+
except AttributeError:
|
|
139
|
+
pass
|
|
140
|
+
return fallback
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def get_project_metadata(pyproject: Path = Path("pyproject.toml")) -> ProjectMetadata:
|
|
144
|
+
path = pyproject.resolve()
|
|
145
|
+
cached = _METADATA_CACHE.get(path)
|
|
146
|
+
if cached is not None:
|
|
147
|
+
return cached
|
|
148
|
+
|
|
149
|
+
data = _load_pyproject(pyproject)
|
|
150
|
+
project = data.get("project", {}) if isinstance(data, dict) else {}
|
|
151
|
+
name = str(project.get("name") or pyproject.stem).strip() or "project"
|
|
152
|
+
slug = _normalize_slug(name)
|
|
153
|
+
|
|
154
|
+
urls = project.get("urls", {}) if isinstance(project, dict) else {}
|
|
155
|
+
repo_url = str(urls.get("Repository") or "")
|
|
156
|
+
homepage = str(urls.get("Homepage") or project.get("homepage") or "")
|
|
157
|
+
repo_host = repo_owner = repo_name = ""
|
|
158
|
+
if repo_url:
|
|
159
|
+
parsed = urlparse(repo_url)
|
|
160
|
+
repo_host = parsed.netloc.lower()
|
|
161
|
+
repo_path = parsed.path.strip("/")
|
|
162
|
+
if repo_path.endswith(".git"):
|
|
163
|
+
repo_path = repo_path[:-4]
|
|
164
|
+
parts = [p for p in repo_path.split("/") if p]
|
|
165
|
+
if len(parts) >= 2:
|
|
166
|
+
repo_owner, repo_name = parts[0], parts[1]
|
|
167
|
+
|
|
168
|
+
import_package = _derive_import_package(data, name)
|
|
169
|
+
coverage_source = _derive_coverage_source(data, import_package)
|
|
170
|
+
|
|
171
|
+
meta = ProjectMetadata(
|
|
172
|
+
name=name,
|
|
173
|
+
slug=slug,
|
|
174
|
+
repo_url=repo_url,
|
|
175
|
+
repo_host=repo_host,
|
|
176
|
+
repo_owner=repo_owner,
|
|
177
|
+
repo_name=repo_name,
|
|
178
|
+
homepage=homepage,
|
|
179
|
+
import_package=import_package,
|
|
180
|
+
coverage_source=coverage_source,
|
|
181
|
+
)
|
|
182
|
+
_METADATA_CACHE[path] = meta
|
|
183
|
+
return meta
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def read_version_from_pyproject(pyproject: Path = Path("pyproject.toml")) -> str:
|
|
187
|
+
data = _load_pyproject(pyproject)
|
|
188
|
+
project = data.get("project", {}) if isinstance(data, dict) else {}
|
|
189
|
+
version = str(project.get("version") or "").strip()
|
|
190
|
+
if version:
|
|
191
|
+
return version
|
|
192
|
+
text = pyproject.read_text(encoding="utf-8")
|
|
193
|
+
match = re.search(r'(?m)^version\s*=\s*"([0-9]+(?:\.[0-9]+){2})"', text)
|
|
194
|
+
return match.group(1) if match else ""
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def ensure_clean_git_tree() -> None:
|
|
198
|
+
dirty = subprocess.call(["bash", "-lc", "! git diff --quiet || ! git diff --cached --quiet"], stdout=subprocess.DEVNULL)
|
|
199
|
+
if dirty == 0:
|
|
200
|
+
print("[release] Working tree not clean. Commit or stash changes first.", file=sys.stderr)
|
|
201
|
+
raise SystemExit(1)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def git_branch() -> str:
|
|
205
|
+
return run(["git", "rev-parse", "--abbrev-ref", "HEAD"], capture=True).out.strip()
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def git_delete_tag(name: str, *, remote: str | None = None) -> None:
|
|
209
|
+
run(["git", "tag", "-d", name], check=False, capture=True)
|
|
210
|
+
if remote:
|
|
211
|
+
run(["git", "push", remote, f":refs/tags/{name}"], check=False)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def git_tag_exists(name: str) -> bool:
|
|
215
|
+
return subprocess.call(["bash", "-lc", f"git rev-parse -q --verify {shlex.quote('refs/tags/' + name)} >/dev/null"], stdout=subprocess.DEVNULL) == 0
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def git_create_annotated_tag(name: str, message: str) -> None:
|
|
219
|
+
run(["git", "tag", "-a", name, "-m", message])
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def git_push(remote: str, ref: str) -> None:
|
|
223
|
+
run(["git", "push", remote, ref])
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def gh_available() -> bool:
|
|
227
|
+
return cmd_exists("gh")
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def gh_release_exists(tag: str) -> bool:
|
|
231
|
+
return subprocess.call(["bash", "-lc", f"gh release view {shlex.quote(tag)} >/dev/null 2>&1"], stdout=subprocess.DEVNULL) == 0
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def gh_release_create(tag: str, title: str, body: str) -> None:
|
|
235
|
+
run(["gh", "release", "create", tag, "-t", title, "-n", body], check=False)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def gh_release_edit(tag: str, title: str, body: str) -> None:
|
|
239
|
+
run(["gh", "release", "edit", tag, "-t", title, "-n", body], check=False)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def sync_packaging() -> None:
|
|
243
|
+
run([sys.executable, "scripts/bump_version.py", "--sync-packaging"], check=False)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def bootstrap_dev() -> None:
|
|
247
|
+
if not (cmd_exists("ruff") and cmd_exists("pyright")):
|
|
248
|
+
print("[bootstrap] Installing dev dependencies via 'pip install -e .[dev]'")
|
|
249
|
+
run([sys.executable, "-m", "pip", "install", "-e", ".[dev]"])
|
|
250
|
+
try:
|
|
251
|
+
from importlib import import_module
|
|
252
|
+
|
|
253
|
+
import_module("sqlite3")
|
|
254
|
+
except Exception:
|
|
255
|
+
run([sys.executable, "-m", "pip", "install", "pysqlite3-binary"], check=False)
|
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import click
|
|
4
4
|
import sys
|
|
5
5
|
|
|
6
|
-
from ._utils import cmd_exists, run
|
|
6
|
+
from ._utils import cmd_exists, get_project_metadata, run
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
@click.command(help="Build wheel/sdist, optionally attempt conda/brew/nix builds if tools present")
|
|
@@ -14,6 +14,8 @@ def main(conda: bool, brew: bool, nix: bool) -> None:
|
|
|
14
14
|
click.echo("[1/4] Building wheel/sdist via python -m build")
|
|
15
15
|
run([sys.executable, "-m", "build"]) # requires build in dev deps
|
|
16
16
|
|
|
17
|
+
project = get_project_metadata()
|
|
18
|
+
|
|
17
19
|
click.echo("[2/4] Attempting conda-build")
|
|
18
20
|
if conda and cmd_exists("conda"):
|
|
19
21
|
run(["bash", "-lc", "CONDA_USE_LOCAL=1 conda build packaging/conda/recipe"], check=False)
|
|
@@ -22,7 +24,7 @@ def main(conda: bool, brew: bool, nix: bool) -> None:
|
|
|
22
24
|
|
|
23
25
|
click.echo("[3/4] Attempting Homebrew build/install from local formula")
|
|
24
26
|
if brew and cmd_exists("brew"):
|
|
25
|
-
run(["bash", "-lc", "brew install --build-from-source
|
|
27
|
+
run(["bash", "-lc", f"brew install --build-from-source {project.brew_formula_path}"], check=False)
|
|
26
28
|
else:
|
|
27
29
|
click.echo("[brew] skipping: Homebrew not available or disabled")
|
|
28
30
|
|
|
@@ -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:
|