lib-cli-exit-tools 1.0.3__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.
Files changed (56) hide show
  1. {lib_cli_exit_tools-1.0.3 → lib_cli_exit_tools-1.1.0}/CHANGELOG.md +4 -0
  2. {lib_cli_exit_tools-1.0.3 → lib_cli_exit_tools-1.1.0}/PKG-INFO +33 -1
  3. {lib_cli_exit_tools-1.0.3 → lib_cli_exit_tools-1.1.0}/README.md +31 -0
  4. {lib_cli_exit_tools-1.0.3 → lib_cli_exit_tools-1.1.0}/packaging/brew/Formula/lib-cli-exit-tools.rb +1 -1
  5. {lib_cli_exit_tools-1.0.3 → lib_cli_exit_tools-1.1.0}/packaging/conda/recipe/meta.yaml +1 -1
  6. {lib_cli_exit_tools-1.0.3 → lib_cli_exit_tools-1.1.0}/packaging/nix/flake.nix +2 -2
  7. {lib_cli_exit_tools-1.0.3 → lib_cli_exit_tools-1.1.0}/pyproject.toml +2 -1
  8. lib_cli_exit_tools-1.1.0/scripts/_utils.py +255 -0
  9. {lib_cli_exit_tools-1.0.3 → lib_cli_exit_tools-1.1.0}/scripts/build.py +4 -2
  10. {lib_cli_exit_tools-1.0.3 → lib_cli_exit_tools-1.1.0}/scripts/bump_version.py +38 -29
  11. {lib_cli_exit_tools-1.0.3 → lib_cli_exit_tools-1.1.0}/scripts/menu.py +9 -1
  12. lib_cli_exit_tools-1.1.0/scripts/run_cli.py +26 -0
  13. {lib_cli_exit_tools-1.0.3 → lib_cli_exit_tools-1.1.0}/scripts/test.py +41 -8
  14. lib_cli_exit_tools-1.1.0/src/lib_cli_exit_tools/__init__.py +33 -0
  15. lib_cli_exit_tools-1.1.0/src/lib_cli_exit_tools/__main__.py +4 -0
  16. lib_cli_exit_tools-1.1.0/src/lib_cli_exit_tools/cli.py +45 -0
  17. lib_cli_exit_tools-1.1.0/src/lib_cli_exit_tools/lib_cli_exit_tools.py +372 -0
  18. lib_cli_exit_tools-1.1.0/tests/test_cli.py +85 -0
  19. lib_cli_exit_tools-1.1.0/tests/test_cli_extra.py +32 -0
  20. lib_cli_exit_tools-1.1.0/tests/test_extend_cli.py +111 -0
  21. lib_cli_exit_tools-1.0.3/scripts/_utils.py +0 -125
  22. lib_cli_exit_tools-1.0.3/scripts/run_cli.py +0 -17
  23. lib_cli_exit_tools-1.0.3/src/lib_cli_exit_tools/__init__.py +0 -15
  24. lib_cli_exit_tools-1.0.3/src/lib_cli_exit_tools/__main__.py +0 -4
  25. lib_cli_exit_tools-1.0.3/src/lib_cli_exit_tools/cli.py +0 -138
  26. lib_cli_exit_tools-1.0.3/src/lib_cli_exit_tools/lib_cli_exit_tools.py +0 -353
  27. lib_cli_exit_tools-1.0.3/tests/test_cli.py +0 -49
  28. lib_cli_exit_tools-1.0.3/tests/test_cli_extra.py +0 -54
  29. {lib_cli_exit_tools-1.0.3 → lib_cli_exit_tools-1.1.0}/.env.example +0 -0
  30. {lib_cli_exit_tools-1.0.3 → lib_cli_exit_tools-1.1.0}/.github/workflows/ci.yml +0 -0
  31. {lib_cli_exit_tools-1.0.3 → lib_cli_exit_tools-1.1.0}/.github/workflows/codeql.yml +0 -0
  32. {lib_cli_exit_tools-1.0.3 → lib_cli_exit_tools-1.1.0}/.github/workflows/release.yml +0 -0
  33. {lib_cli_exit_tools-1.0.3 → lib_cli_exit_tools-1.1.0}/.gitignore +0 -0
  34. {lib_cli_exit_tools-1.0.3 → lib_cli_exit_tools-1.1.0}/AGENTS.md +0 -0
  35. {lib_cli_exit_tools-1.0.3 → lib_cli_exit_tools-1.1.0}/CONTRIBUTING.md +0 -0
  36. {lib_cli_exit_tools-1.0.3 → lib_cli_exit_tools-1.1.0}/LICENSE +0 -0
  37. {lib_cli_exit_tools-1.0.3 → lib_cli_exit_tools-1.1.0}/Makefile +0 -0
  38. {lib_cli_exit_tools-1.0.3 → lib_cli_exit_tools-1.1.0}/codecov.yml +0 -0
  39. {lib_cli_exit_tools-1.0.3 → lib_cli_exit_tools-1.1.0}/notebooks/Quickstart.ipynb +0 -0
  40. {lib_cli_exit_tools-1.0.3 → lib_cli_exit_tools-1.1.0}/packaging/README.md +0 -0
  41. {lib_cli_exit_tools-1.0.3 → lib_cli_exit_tools-1.1.0}/qlty.toml +0 -0
  42. {lib_cli_exit_tools-1.0.3 → lib_cli_exit_tools-1.1.0}/scripts/__init__.py +0 -0
  43. {lib_cli_exit_tools-1.0.3 → lib_cli_exit_tools-1.1.0}/scripts/bump.py +0 -0
  44. {lib_cli_exit_tools-1.0.3 → lib_cli_exit_tools-1.1.0}/scripts/bump_major.py +0 -0
  45. {lib_cli_exit_tools-1.0.3 → lib_cli_exit_tools-1.1.0}/scripts/bump_minor.py +0 -0
  46. {lib_cli_exit_tools-1.0.3 → lib_cli_exit_tools-1.1.0}/scripts/bump_patch.py +0 -0
  47. {lib_cli_exit_tools-1.0.3 → lib_cli_exit_tools-1.1.0}/scripts/clean.py +0 -0
  48. {lib_cli_exit_tools-1.0.3 → lib_cli_exit_tools-1.1.0}/scripts/dev.py +0 -0
  49. {lib_cli_exit_tools-1.0.3 → lib_cli_exit_tools-1.1.0}/scripts/install.py +0 -0
  50. {lib_cli_exit_tools-1.0.3 → lib_cli_exit_tools-1.1.0}/scripts/push.py +0 -0
  51. {lib_cli_exit_tools-1.0.3 → lib_cli_exit_tools-1.1.0}/scripts/release.py +0 -0
  52. {lib_cli_exit_tools-1.0.3 → lib_cli_exit_tools-1.1.0}/scripts/version_current.py +0 -0
  53. {lib_cli_exit_tools-1.0.3 → lib_cli_exit_tools-1.1.0}/src/lib_cli_exit_tools/__init__conf__.py +0 -0
  54. {lib_cli_exit_tools-1.0.3 → lib_cli_exit_tools-1.1.0}/src/lib_cli_exit_tools/py.typed +0 -0
  55. {lib_cli_exit_tools-1.0.3 → lib_cli_exit_tools-1.1.0}/tests/test_exit_tools.py +0 -0
  56. {lib_cli_exit_tools-1.0.3 → lib_cli_exit_tools-1.1.0}/tests/test_lib_extra.py +0 -0
@@ -1,5 +1,9 @@
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
+
3
7
  ## [1.0.3] - 2025-09-16
4
8
 
5
9
  - added make menu
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lib_cli_exit_tools
3
- Version: 1.0.3
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
 
@@ -230,6 +231,37 @@ cli-exit-tools --help # alias
230
231
  lib_cli_exit_tools info
231
232
  ```
232
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
+
233
265
  Library:
234
266
 
235
267
  ```python
@@ -200,6 +200,37 @@ cli-exit-tools --help # alias
200
200
  lib_cli_exit_tools info
201
201
  ```
202
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
+
203
234
  Library:
204
235
 
205
236
  ```python
@@ -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.3.tar.gz"
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
 
@@ -1,5 +1,5 @@
1
1
  {% set name = "lib_cli_exit_tools" %}
2
- {% set version = "1.0.3" %}
2
+ {% set version = "1.1.0" %}
3
3
 
4
4
  package:
5
5
  name: {{ name|lower }}
@@ -35,7 +35,7 @@
35
35
  {
36
36
  packages.default = pypkgs.buildPythonPackage {
37
37
  pname = "lib_cli_exit_tools";
38
- version = "1.0.3";
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.3";
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"
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]
@@ -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 packaging/brew/Formula/lib-cli-exit-tools.rb"], check=False)
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
 
@@ -8,6 +8,10 @@ from typing import Any, Dict, Tuple, Optional, cast
8
8
  import json
9
9
  import urllib.request
10
10
  import hashlib
11
+ import sys
12
+
13
+ sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
14
+ from scripts._utils import get_project_metadata # noqa: E402
11
15
 
12
16
  # Optional TOML parser for Python < 3.11 support during type checking
13
17
  try: # pragma: no cover - import resolution
@@ -18,6 +22,8 @@ except Exception: # pragma: no cover - best-effort fallback
18
22
  except Exception: # noqa: PLC1901
19
23
  _tomllib = None # type: ignore[assignment]
20
24
 
25
+ PROJECT_META = get_project_metadata()
26
+
21
27
 
22
28
  def parse_args() -> argparse.Namespace:
23
29
  p = argparse.ArgumentParser(description="Bump version in pyproject.toml and CHANGELOG.md")
@@ -221,19 +227,20 @@ def _update_conda_recipe(version: str, path: Path) -> None:
221
227
  text = new_text
222
228
  changed = True
223
229
  # Attempt to set sha256 for remote source tarball (used when COND A_USE_LOCAL != 1)
224
- try:
225
- tar_url = f"https://github.com/bitranox/lib_cli_exit_tools/archive/refs/tags/v{version}.tar.gz"
226
- with urllib.request.urlopen(tar_url, timeout=10) as resp:
227
- data = resp.read()
228
- sha = hashlib.sha256(data).hexdigest()
229
- text_sha = re.sub(r'(sha256:\s*")([^"]*)(")', rf"\1{sha}\3", text)
230
- if text_sha != text:
231
- text = text_sha
232
- changed = True
233
- print(f"[bump] conda recipe: sha256 updated for v{version}")
234
- except Exception:
235
- # Network may be unavailable; leave sha256 as-is
236
- pass
230
+ tar_url = PROJECT_META.github_tarball_url(version)
231
+ if tar_url:
232
+ try:
233
+ with urllib.request.urlopen(tar_url, timeout=10) as resp:
234
+ data = resp.read()
235
+ sha = hashlib.sha256(data).hexdigest()
236
+ text_sha = re.sub(r'(sha256:\s*")([^"]*)(")', rf"\1{sha}\3", text)
237
+ if text_sha != text:
238
+ text = text_sha
239
+ changed = True
240
+ print(f"[bump] conda recipe: sha256 updated for v{version}")
241
+ except Exception:
242
+ # Network may be unavailable; leave sha256 as-is
243
+ pass
237
244
 
238
245
  if changed:
239
246
  path.write_text(text, encoding="utf-8")
@@ -248,18 +255,19 @@ def _brew_set_source_tag(text: str, version: str) -> tuple[str, bool]:
248
255
  changed = True
249
256
  text = new_text
250
257
  # Try to fetch tarball and compute sha256; replace first sha256 occurrence (main formula)
251
- try:
252
- tar_url = f"https://github.com/bitranox/lib_cli_exit_tools/archive/refs/tags/v{version}.tar.gz"
253
- with urllib.request.urlopen(tar_url, timeout=10) as resp:
254
- data = resp.read()
255
- sha = hashlib.sha256(data).hexdigest()
256
- pattern = r'(^\s*sha256\s+")([^"]+)(")'
257
- updated = re.sub(pattern, rf"\1{sha}\3", text, count=1, flags=re.M)
258
- if updated != text:
259
- text = updated
260
- changed = True
261
- except Exception:
262
- pass
258
+ tar_url = PROJECT_META.github_tarball_url(version)
259
+ if tar_url:
260
+ try:
261
+ with urllib.request.urlopen(tar_url, timeout=10) as resp:
262
+ data = resp.read()
263
+ sha = hashlib.sha256(data).hexdigest()
264
+ pattern = r'(^\s*sha256\s+")([^"]+)(")'
265
+ updated = re.sub(pattern, rf"\1{sha}\3", text, count=1, flags=re.M)
266
+ if updated != text:
267
+ text = updated
268
+ changed = True
269
+ except Exception:
270
+ pass
263
271
  return text, changed
264
272
 
265
273
 
@@ -338,12 +346,13 @@ def _update_nix_flake(version: str, path: Path) -> None:
338
346
  return
339
347
 
340
348
  # Replace version = "X.Y.Z";
341
- # Update package block version only for pname = "lib_cli_exit_tools"
349
+ # Update package block version only for the configured pname from pyproject
342
350
  def repl_pkg_block(m: re.Match[str]) -> str:
343
351
  return f"{m.group(1)}{version}{m.group(3)}"
344
352
 
353
+ name_pattern = re.escape(PROJECT_META.name)
345
354
  t1 = re.sub(
346
- r"(pname\s*=\s*\"lib_cli_exit_tools\";\s*[^}]*?\bversion\s*=\s*\")(\d+\.\d+\.\d+)(\";)",
355
+ rf"(pname\s*=\s*\"{name_pattern}\";\s*[^}}]*?\bversion\s*=\s*\")(\d+\.\d+\.\d+)(\";)",
347
356
  repl_pkg_block,
348
357
  text,
349
358
  flags=re.S,
@@ -398,7 +407,7 @@ def main() -> int:
398
407
  raise SystemExit("version not found in pyproject.toml")
399
408
  target = m.group(1)
400
409
  _update_conda_recipe(target, Path("packaging/conda/recipe/meta.yaml"))
401
- _update_brew_formula(target, Path("packaging/brew/Formula/lib-cli-exit-tools.rb"))
410
+ _update_brew_formula(target, Path(PROJECT_META.brew_formula_path))
402
411
  _update_nix_flake(target, Path("packaging/nix/flake.nix"))
403
412
  return 0
404
413
 
@@ -429,7 +438,7 @@ def main() -> int:
429
438
 
430
439
  # Also bump packaging skeletons if present
431
440
  _update_conda_recipe(target, Path("packaging/conda/recipe/meta.yaml"))
432
- _update_brew_formula(target, Path("packaging/brew/Formula/lib-cli-exit-tools.rb"))
441
+ _update_brew_formula(target, Path(PROJECT_META.brew_formula_path))
433
442
  _update_nix_flake(target, Path("packaging/nix/flake.nix"))
434
443
  return 0
435
444
 
@@ -4,12 +4,20 @@ import asyncio
4
4
  import os
5
5
  import signal
6
6
  from dataclasses import dataclass
7
+ from pathlib import Path
7
8
  from typing import TYPE_CHECKING, Callable, Dict, Optional
8
9
 
9
10
  import contextlib
10
11
 
11
12
  from rich.text import Text
12
13
 
14
+ import sys
15
+
16
+ sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
17
+ from scripts._utils import get_project_metadata # noqa: E402
18
+
19
+ PROJECT = get_project_metadata()
20
+
13
21
  if TYPE_CHECKING:
14
22
  from textual import events
15
23
  from textual.app import App, ComposeResult
@@ -170,7 +178,7 @@ class MenuScreen(Screen[None]):
170
178
  def compose(self) -> ComposeResult: # type: ignore[override]
171
179
  with Container(id="backdrop"):
172
180
  with Vertical(id="window", classes="whiptail-window"):
173
- yield Static("lib_cli_exit_tools — Make Targets", id="title")
181
+ yield Static(f"{PROJECT.name} — Make Targets", id="title")
174
182
  self._description = Static("Select a target and press Enter.", id="description")
175
183
  yield self._description
176
184
  yield self._build_list()
@@ -0,0 +1,26 @@
1
+ from __future__ import annotations
2
+
3
+ import click
4
+ import sys
5
+ from importlib import import_module
6
+ from pathlib import Path
7
+
8
+ sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
9
+ from scripts._utils import get_project_metadata # noqa: E402
10
+
11
+ PROJECT = get_project_metadata()
12
+ PACKAGE = PROJECT.import_package
13
+
14
+
15
+ @click.command(help=f"Run {PROJECT.name} CLI (passes additional args)")
16
+ @click.argument("args", nargs=-1)
17
+ def main(args: tuple[str, ...]) -> None:
18
+ import_module(f"{PACKAGE}.__main__")
19
+ cli_main = import_module(f"{PACKAGE}.cli").main
20
+
21
+ code = cli_main(list(args) if args else ["--help"]) # returns int
22
+ raise SystemExit(int(code))
23
+
24
+
25
+ if __name__ == "__main__":
26
+ main()