lib-cli-exit-tools 2.0.0__tar.gz → 2.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-2.0.0 → lib_cli_exit_tools-2.1.0}/AGENTS.md +2 -2
- {lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/CHANGELOG.md +7 -0
- {lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/DEVELOPMENT.md +4 -4
- {lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/INSTALL.md +2 -2
- {lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/PKG-INFO +10 -9
- {lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/README.md +9 -8
- {lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/docs/system-design/module_reference.md +5 -5
- {lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/notebooks/Quickstart.ipynb +1 -1
- {lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/pyproject.toml +1 -1
- {lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/scripts/_utils.py +151 -9
- {lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/scripts/build.py +2 -1
- {lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/scripts/push.py +8 -1
- {lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/scripts/test.py +17 -23
- lib_cli_exit_tools-2.1.0/src/lib_cli_exit_tools/__init__conf__.py +67 -0
- {lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/src/lib_cli_exit_tools/cli.py +1 -1
- {lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/tests/test_cli.py +12 -0
- lib_cli_exit_tools-2.1.0/tests/test_metadata.py +47 -0
- lib_cli_exit_tools-2.0.0/src/lib_cli_exit_tools/__init__conf__.py +0 -220
- lib_cli_exit_tools-2.0.0/tests/test_metadata.py +0 -101
- {lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/.codacy/cli-config.yaml +0 -0
- {lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/.codacy/cli.sh +0 -0
- {lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/.codacy/codacy.yaml +0 -0
- {lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/.codacy/tools-configs/lizard.yaml +0 -0
- {lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/.codacy/tools-configs/trivy.yaml +0 -0
- {lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/.devcontainer/devcontainer.json +0 -0
- {lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/.devcontainer/settings.json +0 -0
- {lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/.env.example +0 -0
- {lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/.github/dependabot.yml +0 -0
- {lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/.github/workflows/ci.yml +0 -0
- {lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/.github/workflows/codeql.yml +0 -0
- {lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/.github/workflows/release.yml +0 -0
- {lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/.gitignore +0 -0
- {lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/.qlty/qlty.toml +0 -0
- {lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/.snyk +0 -0
- {lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/AGENTS_README.md +0 -0
- {lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/CONTRIBUTING.md +0 -0
- {lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/LICENSE +0 -0
- {lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/Makefile +0 -0
- {lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/codecov.yml +0 -0
- {lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/scripts/__init__.py +0 -0
- {lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/scripts/__main__.py +0 -0
- {lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/scripts/bump.py +0 -0
- {lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/scripts/bump_major.py +0 -0
- {lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/scripts/bump_minor.py +0 -0
- {lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/scripts/bump_patch.py +0 -0
- {lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/scripts/bump_version.py +0 -0
- {lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/scripts/clean.py +0 -0
- {lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/scripts/cli.py +0 -0
- {lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/scripts/dev.py +0 -0
- {lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/scripts/help.py +0 -0
- {lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/scripts/install.py +0 -0
- {lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/scripts/menu.py +0 -0
- {lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/scripts/release.py +0 -0
- {lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/scripts/run_cli.py +0 -0
- {lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/scripts/target_metadata.py +0 -0
- {lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/scripts/version_current.py +0 -0
- {lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/src/lib_cli_exit_tools/__init__.py +0 -0
- {lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/src/lib_cli_exit_tools/__main__.py +0 -0
- {lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/src/lib_cli_exit_tools/adapters/__init__.py +0 -0
- {lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/src/lib_cli_exit_tools/adapters/signals.py +0 -0
- {lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/src/lib_cli_exit_tools/application/__init__.py +0 -0
- {lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/src/lib_cli_exit_tools/application/runner.py +0 -0
- {lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/src/lib_cli_exit_tools/core/__init__.py +0 -0
- {lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/src/lib_cli_exit_tools/core/configuration.py +0 -0
- {lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/src/lib_cli_exit_tools/core/exit_codes.py +0 -0
- {lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/src/lib_cli_exit_tools/lib_cli_exit_tools.py +0 -0
- {lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/src/lib_cli_exit_tools/py.typed +0 -0
- {lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/tests/conftest.py +0 -0
- {lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/tests/test_adapters_signals.py +0 -0
- {lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/tests/test_application_runner.py +0 -0
- {lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/tests/test_behaviors.py +0 -0
- {lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/tests/test_core_configuration.py +0 -0
- {lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/tests/test_core_exit_codes.py +0 -0
- {lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/tests/test_module_entry.py +0 -0
- {lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/tests/test_scripts.py +0 -0
- {lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/tests/test_signals_integration.py +0 -0
|
@@ -50,8 +50,8 @@ when writing or refracturing Python scripts, apply those Rules :
|
|
|
50
50
|
### Versioning & Releases
|
|
51
51
|
|
|
52
52
|
- Single source of truth for the package version is `pyproject.toml` (`[project].version`).
|
|
53
|
-
-
|
|
54
|
-
-
|
|
53
|
+
- Automation rewrites `src/lib_cli_exit_tools/__init__conf__.py` from `pyproject.toml`, so runtime code imports generated constants instead of querying `importlib.metadata`.
|
|
54
|
+
- After updating project metadata (version, summary, URLs, authors) run `make test` (or `python -m scripts.test`) to regenerate the metadata module before committing.
|
|
55
55
|
- Tag releases `vX.Y.Z` and push tags; CI will build artifacts and publish when configured.
|
|
56
56
|
|
|
57
57
|
### Common Make Targets (Alphabetical)
|
|
@@ -3,6 +3,13 @@
|
|
|
3
3
|
All notable changes to this project are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
4
4
|
|
|
5
5
|
|
|
6
|
+
## [2.1.0] - 2025-10-13
|
|
7
|
+
|
|
8
|
+
### Changed
|
|
9
|
+
- `scripts.build` now calls `sync_metadata_module()` before invoking `python -m build`, ensuring wheels and sdists ship freshly generated metadata.
|
|
10
|
+
- Added an explicit CLI test covering the `--version` flag so console-script aliases stay aligned with generated constants.
|
|
11
|
+
- Updated README, INSTALL instructions, and the Quickstart notebook to document `lib-cli-exit-tools` as the canonical console entry point alongside its aliases.
|
|
12
|
+
|
|
6
13
|
## [2.0.0] - 2025-10-12
|
|
7
14
|
|
|
8
15
|
### Changed
|
|
@@ -118,10 +118,10 @@ keep the signal-handling guarantees honest.
|
|
|
118
118
|
|
|
119
119
|
## Versioning & Metadata
|
|
120
120
|
|
|
121
|
-
-
|
|
122
|
-
-
|
|
123
|
-
-
|
|
124
|
-
- Console script name is discovered from entry points;
|
|
121
|
+
- `pyproject.toml` (`[project]`) remains the single source of truth for name, version, homepage, and authors.
|
|
122
|
+
- `scripts._utils.sync_metadata_module()` rewrites `src/lib_cli_exit_tools/__init__conf__.py` so runtime code consumes generated constants instead of `importlib.metadata`.
|
|
123
|
+
- After editing project metadata (name, version, description, URLs, authors), run `make test` or `python -m scripts.test` to regenerate the module before committing.
|
|
124
|
+
- Console script name is still discovered from entry points; automation falls back to `lib_cli_exit_tools` when no explicit alias exists.
|
|
125
125
|
|
|
126
126
|
## CI & Publishing
|
|
127
127
|
|
|
@@ -85,7 +85,7 @@ pipx install "git+https://github.com/bitranox/lib_cli_exit_tools@vX.Y.Z"
|
|
|
85
85
|
```bash
|
|
86
86
|
uv pip install -e .[dev]
|
|
87
87
|
uv tool install lib_cli_exit_tools
|
|
88
|
-
uvx
|
|
88
|
+
uvx lib-cli-exit-tools --help # aliases: cli-exit-tools, lib_cli_exit_tools
|
|
89
89
|
```
|
|
90
90
|
|
|
91
91
|
### Installing from local artifacts
|
|
@@ -153,7 +153,7 @@ error: externally-managed-environment
|
|
|
153
153
|
|
|
154
154
|
### Missing command on PATH
|
|
155
155
|
|
|
156
|
-
If `
|
|
156
|
+
If `lib-cli-exit-tools --help` (or the aliases `cli-exit-tools` / `lib_cli_exit_tools`) returns “command not found”, confirm that the installation location is on PATH. The most common fix is adding `~/.local/bin` (POSIX) or `%APPDATA%\Python\PythonXY\Scripts` (Windows) to your PATH environment variable, then re-opening the shell.
|
|
157
157
|
|
|
158
158
|
### Colour output not appearing
|
|
159
159
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lib_cli_exit_tools
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.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
|
|
@@ -65,14 +65,14 @@ See [INSTALL.md](INSTALL.md) for editable installs, pipx/uv usage, and troublesh
|
|
|
65
65
|
|
|
66
66
|
## Usage
|
|
67
67
|
|
|
68
|
-
Console script:
|
|
68
|
+
Console script (all commands map to ``lib_cli_exit_tools.cli:main``):
|
|
69
69
|
|
|
70
70
|
```bash
|
|
71
71
|
# After install (pip/pipx/uv tool)
|
|
72
|
-
|
|
73
|
-
cli-exit-tools
|
|
74
|
-
|
|
75
|
-
|
|
72
|
+
lib-cli-exit-tools --help
|
|
73
|
+
lib-cli-exit-tools info
|
|
74
|
+
lib-cli-exit-tools fail # intentionally trigger RuntimeError to test error paths
|
|
75
|
+
# Aliases are also generated: `cli-exit-tools`, `lib_cli_exit_tools`
|
|
76
76
|
```
|
|
77
77
|
|
|
78
78
|
### Examples
|
|
@@ -280,9 +280,10 @@ except Exception as e:
|
|
|
280
280
|
raise SystemExit(code)
|
|
281
281
|
```
|
|
282
282
|
|
|
283
|
-
Command names registered on install
|
|
284
|
-
-
|
|
283
|
+
Command names registered on install (all invoke ``lib_cli_exit_tools.cli:main``)
|
|
284
|
+
- lib-cli-exit-tools (default console script)
|
|
285
285
|
- cli-exit-tools (alias)
|
|
286
|
+
- lib_cli_exit_tools (alias)
|
|
286
287
|
- python -m lib_cli_exit_tools (module entry)
|
|
287
288
|
|
|
288
289
|
If you installed with --user or in a venv, make sure the corresponding bin directory is on PATH:
|
|
@@ -562,7 +563,7 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|
|
562
563
|
)
|
|
563
564
|
```
|
|
564
565
|
|
|
565
|
-
When installed, the generated console scripts (`
|
|
566
|
+
When installed, the generated console scripts (`lib-cli-exit-tools`, `cli-exit-tools`, `lib_cli_exit_tools`) will import `your_package.cli:main`, and `python -m your_package` will follow the same code path via `__main__.py`.
|
|
566
567
|
|
|
567
568
|
## Exit Codes
|
|
568
569
|
|
|
@@ -29,14 +29,14 @@ See [INSTALL.md](INSTALL.md) for editable installs, pipx/uv usage, and troublesh
|
|
|
29
29
|
|
|
30
30
|
## Usage
|
|
31
31
|
|
|
32
|
-
Console script:
|
|
32
|
+
Console script (all commands map to ``lib_cli_exit_tools.cli:main``):
|
|
33
33
|
|
|
34
34
|
```bash
|
|
35
35
|
# After install (pip/pipx/uv tool)
|
|
36
|
-
|
|
37
|
-
cli-exit-tools
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
lib-cli-exit-tools --help
|
|
37
|
+
lib-cli-exit-tools info
|
|
38
|
+
lib-cli-exit-tools fail # intentionally trigger RuntimeError to test error paths
|
|
39
|
+
# Aliases are also generated: `cli-exit-tools`, `lib_cli_exit_tools`
|
|
40
40
|
```
|
|
41
41
|
|
|
42
42
|
### Examples
|
|
@@ -244,9 +244,10 @@ except Exception as e:
|
|
|
244
244
|
raise SystemExit(code)
|
|
245
245
|
```
|
|
246
246
|
|
|
247
|
-
Command names registered on install
|
|
248
|
-
-
|
|
247
|
+
Command names registered on install (all invoke ``lib_cli_exit_tools.cli:main``)
|
|
248
|
+
- lib-cli-exit-tools (default console script)
|
|
249
249
|
- cli-exit-tools (alias)
|
|
250
|
+
- lib_cli_exit_tools (alias)
|
|
250
251
|
- python -m lib_cli_exit_tools (module entry)
|
|
251
252
|
|
|
252
253
|
If you installed with --user or in a venv, make sure the corresponding bin directory is on PATH:
|
|
@@ -526,7 +527,7 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|
|
526
527
|
)
|
|
527
528
|
```
|
|
528
529
|
|
|
529
|
-
When installed, the generated console scripts (`
|
|
530
|
+
When installed, the generated console scripts (`lib-cli-exit-tools`, `cli-exit-tools`, `lib_cli_exit_tools`) will import `your_package.cli:main`, and `python -m your_package` will follow the same code path via `__main__.py`.
|
|
530
531
|
|
|
531
532
|
## Exit Codes
|
|
532
533
|
|
{lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/docs/system-design/module_reference.md
RENAMED
|
@@ -38,7 +38,7 @@ Downstream CLIs and automation pipelines need deterministic exit codes, signal h
|
|
|
38
38
|
* Split runtime responsibilities into well-documented modules (`core`, `application`, `adapters`, `cli`) that mirror clean-architecture prompts.
|
|
39
39
|
* Provide a lightweight metadata façade (`__init__conf__.py`) so CLI commands expose provenance without heavy packaging dependencies.
|
|
40
40
|
* Funnel all exit handling through `application.runner` and `core.exit_codes`, documenting each resolver path and configuration hook.
|
|
41
|
-
* Publish behaviour-driven tests that read like specifications and explicitly cover CLI surfaces, metadata
|
|
41
|
+
* Publish behaviour-driven tests that read like specifications and explicitly cover CLI surfaces, metadata exports, module entry balance, and automation scripts.
|
|
42
42
|
* Align system documentation with inline docstrings and testing strategy to create a single source of truth for maintainers.
|
|
43
43
|
|
|
44
44
|
---
|
|
@@ -56,7 +56,7 @@ Downstream CLIs and automation pipelines need deterministic exit codes, signal h
|
|
|
56
56
|
5. `core.exit_codes` resolves exit codes via ordered strategies (subprocess return codes, signals, errno/winerror, sysexits, platform tables).
|
|
57
57
|
6. `__init__conf__.py` supplies metadata for `info` commands and documentation output.
|
|
58
58
|
|
|
59
|
-
**System Dependencies:** Standard library (`signal`, `sys`, `subprocess`, `
|
|
59
|
+
**System Dependencies:** Standard library (`signal`, `sys`, `subprocess`, `contextlib`, `dataclasses`), `click`, `rich`, `rich-click`.
|
|
60
60
|
|
|
61
61
|
---
|
|
62
62
|
|
|
@@ -78,9 +78,9 @@ Downstream CLIs and automation pipelines need deterministic exit codes, signal h
|
|
|
78
78
|
|
|
79
79
|
### Module: lib_cli_exit_tools/__init__conf__.py
|
|
80
80
|
|
|
81
|
-
* **Purpose:**
|
|
82
|
-
* **Input:**
|
|
83
|
-
* **Output:** Constants (`name`, `title`, `version`, `homepage`, `author`, `shell_command`) and
|
|
81
|
+
* **Purpose:** Publish static metadata constants and `print_info()` for CLI provenance.
|
|
82
|
+
* **Input:** Generated from `pyproject.toml` by `scripts._utils.sync_metadata_module`; no runtime metadata lookups.
|
|
83
|
+
* **Output:** Constants (`name`, `title`, `version`, `homepage`, `author`, `author_email`, `shell_command`) and the formatted `print_info()` helper.
|
|
84
84
|
* **Location:** `src/lib_cli_exit_tools/__init__conf__.py`
|
|
85
85
|
|
|
86
86
|
### Module: lib_cli_exit_tools/core/configuration.py
|
|
@@ -4,7 +4,7 @@ Purpose
|
|
|
4
4
|
-------
|
|
5
5
|
Collect helper functions used by the ``scripts/`` entry points (build, test,
|
|
6
6
|
release) so git helpers and subprocess wrappers live in one place. The behaviour mirrors the operational guidance described in
|
|
7
|
-
``docs/systemdesign/
|
|
7
|
+
``docs/systemdesign/module_reference.md`` and ``DEVELOPMENT.md``.
|
|
8
8
|
|
|
9
9
|
Contents
|
|
10
10
|
--------
|
|
@@ -21,12 +21,15 @@ avoid duplication and keep CI/CD behaviour consistent with documentation.
|
|
|
21
21
|
|
|
22
22
|
from __future__ import annotations
|
|
23
23
|
|
|
24
|
+
import json
|
|
24
25
|
import os
|
|
25
26
|
import re
|
|
26
27
|
import shlex
|
|
28
|
+
import shutil
|
|
27
29
|
import subprocess
|
|
28
30
|
import sys
|
|
29
31
|
import tomllib
|
|
32
|
+
import textwrap
|
|
30
33
|
from dataclasses import dataclass
|
|
31
34
|
from pathlib import Path
|
|
32
35
|
from subprocess import CompletedProcess
|
|
@@ -54,6 +57,12 @@ class ProjectMetadata:
|
|
|
54
57
|
import_package: str
|
|
55
58
|
coverage_source: str
|
|
56
59
|
scripts: dict[str, str]
|
|
60
|
+
metadata_module: Path
|
|
61
|
+
version: str
|
|
62
|
+
summary: str
|
|
63
|
+
author_name: str
|
|
64
|
+
author_email: str
|
|
65
|
+
shell_command: str
|
|
57
66
|
|
|
58
67
|
def github_tarball_url(self, version: str) -> str:
|
|
59
68
|
if self.repo_host == "github.com" and self.repo_owner and self.repo_name:
|
|
@@ -90,6 +99,7 @@ class ProjectMetadata:
|
|
|
90
99
|
summary.append(f"repository={self.repo_url}")
|
|
91
100
|
if self.homepage:
|
|
92
101
|
summary.append(f"homepage={self.homepage}")
|
|
102
|
+
summary.append(f"version={self.version}")
|
|
93
103
|
return tuple(summary)
|
|
94
104
|
|
|
95
105
|
|
|
@@ -131,14 +141,7 @@ def run(
|
|
|
131
141
|
|
|
132
142
|
|
|
133
143
|
def cmd_exists(name: str) -> bool:
|
|
134
|
-
return (
|
|
135
|
-
subprocess.call(
|
|
136
|
-
["bash", "-lc", f"command -v {shlex.quote(name)} >/dev/null 2>&1"],
|
|
137
|
-
stdout=subprocess.DEVNULL,
|
|
138
|
-
stderr=subprocess.DEVNULL,
|
|
139
|
-
)
|
|
140
|
-
== 0
|
|
141
|
-
)
|
|
144
|
+
return shutil.which(name) is not None
|
|
142
145
|
|
|
143
146
|
|
|
144
147
|
def _normalize_slug(value: str) -> str:
|
|
@@ -313,6 +316,47 @@ def get_project_metadata(pyproject: Path = Path("pyproject.toml")) -> ProjectMet
|
|
|
313
316
|
coverage_source = _derive_coverage_source(data, import_package)
|
|
314
317
|
scripts = _derive_scripts(data)
|
|
315
318
|
|
|
319
|
+
version = read_version_from_pyproject(pyproject)
|
|
320
|
+
summary = description.strip() if description else ""
|
|
321
|
+
if not summary:
|
|
322
|
+
summary_candidate = project_table.get("summary")
|
|
323
|
+
summary = summary_candidate.strip() if isinstance(summary_candidate, str) else ""
|
|
324
|
+
if not summary:
|
|
325
|
+
summary = name
|
|
326
|
+
|
|
327
|
+
author_name = ""
|
|
328
|
+
author_email = ""
|
|
329
|
+
authors_value = project_table.get("authors")
|
|
330
|
+
if isinstance(authors_value, list):
|
|
331
|
+
authors_list = cast(list[object], authors_value)
|
|
332
|
+
for author_entry in authors_list:
|
|
333
|
+
if not isinstance(author_entry, dict):
|
|
334
|
+
continue
|
|
335
|
+
author_dict = cast(dict[str, object], author_entry)
|
|
336
|
+
name_field = author_dict.get("name")
|
|
337
|
+
email_field = author_dict.get("email")
|
|
338
|
+
if not author_name and isinstance(name_field, str) and name_field.strip():
|
|
339
|
+
author_name = name_field.strip()
|
|
340
|
+
if not author_email and isinstance(email_field, str) and email_field.strip():
|
|
341
|
+
author_email = email_field.strip()
|
|
342
|
+
if not author_name:
|
|
343
|
+
author_name = repo_owner or name
|
|
344
|
+
|
|
345
|
+
shell_command = slug.replace("_", "-")
|
|
346
|
+
preferred_entry = _select_cli_entry(
|
|
347
|
+
scripts,
|
|
348
|
+
(
|
|
349
|
+
slug,
|
|
350
|
+
name,
|
|
351
|
+
import_package,
|
|
352
|
+
import_package.replace("_", "-"),
|
|
353
|
+
),
|
|
354
|
+
)
|
|
355
|
+
if preferred_entry is not None:
|
|
356
|
+
shell_command = preferred_entry[0]
|
|
357
|
+
|
|
358
|
+
metadata_module = (Path("src") / import_package / "__init__conf__.py").resolve()
|
|
359
|
+
|
|
316
360
|
meta = ProjectMetadata(
|
|
317
361
|
name=name,
|
|
318
362
|
description=description,
|
|
@@ -325,11 +369,109 @@ def get_project_metadata(pyproject: Path = Path("pyproject.toml")) -> ProjectMet
|
|
|
325
369
|
import_package=import_package,
|
|
326
370
|
coverage_source=coverage_source,
|
|
327
371
|
scripts=scripts,
|
|
372
|
+
metadata_module=metadata_module,
|
|
373
|
+
version=version,
|
|
374
|
+
summary=summary,
|
|
375
|
+
author_name=author_name,
|
|
376
|
+
author_email=author_email,
|
|
377
|
+
shell_command=shell_command,
|
|
328
378
|
)
|
|
329
379
|
_METADATA_CACHE[path] = meta
|
|
330
380
|
return meta
|
|
331
381
|
|
|
332
382
|
|
|
383
|
+
def _quote(value: str) -> str:
|
|
384
|
+
return json.dumps(value, ensure_ascii=False)
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def _render_metadata_module(project: ProjectMetadata) -> str:
|
|
388
|
+
homepage = project.homepage or project.repo_url or ""
|
|
389
|
+
body = f'''"""Static package metadata surfaced to CLI commands and documentation.
|
|
390
|
+
|
|
391
|
+
Purpose
|
|
392
|
+
-------
|
|
393
|
+
Expose the current project metadata as simple constants. These values are kept
|
|
394
|
+
in sync with ``pyproject.toml`` by development automation (tests, push
|
|
395
|
+
pipelines), so runtime code does not query packaging metadata.
|
|
396
|
+
|
|
397
|
+
Contents
|
|
398
|
+
--------
|
|
399
|
+
* Module-level constants describing the published package.
|
|
400
|
+
* :func:`print_info` rendering the constants for the CLI ``info`` command.
|
|
401
|
+
|
|
402
|
+
System Role
|
|
403
|
+
-----------
|
|
404
|
+
Lives in the adapters/platform layer; CLI transports import these constants to
|
|
405
|
+
present authoritative project information without invoking packaging APIs.
|
|
406
|
+
"""
|
|
407
|
+
|
|
408
|
+
from __future__ import annotations
|
|
409
|
+
|
|
410
|
+
#: Distribution name declared in ``pyproject.toml``.
|
|
411
|
+
name = {_quote(project.name)}
|
|
412
|
+
#: Human-readable summary shown in CLI help output.
|
|
413
|
+
title = {_quote(project.summary)}
|
|
414
|
+
#: Current release version pulled from ``pyproject.toml`` by automation.
|
|
415
|
+
version = {_quote(project.version)}
|
|
416
|
+
#: Repository homepage presented to users.
|
|
417
|
+
homepage = {_quote(homepage)}
|
|
418
|
+
#: Author attribution surfaced in CLI output.
|
|
419
|
+
author = {_quote(project.author_name)}
|
|
420
|
+
#: Contact email surfaced in CLI output.
|
|
421
|
+
author_email = {_quote(project.author_email)}
|
|
422
|
+
#: Console-script name published by the package.
|
|
423
|
+
shell_command = {_quote(project.shell_command)}
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def print_info() -> None:
|
|
427
|
+
"""Print the summarised metadata block used by the CLI ``info`` command.
|
|
428
|
+
|
|
429
|
+
Why
|
|
430
|
+
Provides a single, auditable rendering function so documentation and
|
|
431
|
+
CLI output always match the system design reference.
|
|
432
|
+
|
|
433
|
+
Side Effects
|
|
434
|
+
Writes to ``stdout``.
|
|
435
|
+
|
|
436
|
+
Examples
|
|
437
|
+
--------
|
|
438
|
+
>>> print_info() # doctest: +ELLIPSIS
|
|
439
|
+
Info for {project.name}:
|
|
440
|
+
...
|
|
441
|
+
"""
|
|
442
|
+
|
|
443
|
+
fields = [
|
|
444
|
+
("name", name),
|
|
445
|
+
("title", title),
|
|
446
|
+
("version", version),
|
|
447
|
+
("homepage", homepage),
|
|
448
|
+
("author", author),
|
|
449
|
+
("author_email", author_email),
|
|
450
|
+
("shell_command", shell_command),
|
|
451
|
+
]
|
|
452
|
+
pad = max(len(label) for label, _ in fields)
|
|
453
|
+
lines = [f"Info for {{name}}:", ""]
|
|
454
|
+
lines.extend(f" {{label.ljust(pad)}} = {{value}}" for label, value in fields)
|
|
455
|
+
print("\\n".join(lines))
|
|
456
|
+
'''
|
|
457
|
+
return textwrap.dedent(body)
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def sync_metadata_module(project: ProjectMetadata) -> None:
|
|
461
|
+
"""Write ``__init__conf__.py`` so the constants mirror ``pyproject.toml``."""
|
|
462
|
+
|
|
463
|
+
content = _render_metadata_module(project)
|
|
464
|
+
module_path = project.metadata_module
|
|
465
|
+
module_path.parent.mkdir(parents=True, exist_ok=True)
|
|
466
|
+
try:
|
|
467
|
+
existing = module_path.read_text(encoding="utf-8")
|
|
468
|
+
except FileNotFoundError:
|
|
469
|
+
existing = ""
|
|
470
|
+
if existing == content:
|
|
471
|
+
return
|
|
472
|
+
module_path.write_text(content, encoding="utf-8")
|
|
473
|
+
|
|
474
|
+
|
|
333
475
|
def read_version_from_pyproject(pyproject: Path = Path("pyproject.toml")) -> str:
|
|
334
476
|
data = _load_pyproject(pyproject)
|
|
335
477
|
project_table = _as_str_mapping(data.get("project"))
|
|
@@ -4,7 +4,7 @@ import sys
|
|
|
4
4
|
|
|
5
5
|
import rich_click as click
|
|
6
6
|
|
|
7
|
-
from ._utils import get_project_metadata, run
|
|
7
|
+
from ._utils import get_project_metadata, run, sync_metadata_module
|
|
8
8
|
|
|
9
9
|
__all__ = ["build_artifacts"]
|
|
10
10
|
|
|
@@ -22,6 +22,7 @@ def _failure(label: str) -> str:
|
|
|
22
22
|
def build_artifacts() -> None:
|
|
23
23
|
"""Build Python wheel and sdist artifacts."""
|
|
24
24
|
|
|
25
|
+
sync_metadata_module(PROJECT)
|
|
25
26
|
click.echo("[build] Building wheel/sdist via python -m build")
|
|
26
27
|
build_result = run(["python", "-m", "build"], check=False, capture=False)
|
|
27
28
|
click.echo(f"[build] {_status('success') if build_result.code == 0 else _failure('failed')}")
|
|
@@ -6,7 +6,13 @@ from pathlib import Path
|
|
|
6
6
|
|
|
7
7
|
import rich_click as click
|
|
8
8
|
|
|
9
|
-
from ._utils import
|
|
9
|
+
from ._utils import (
|
|
10
|
+
get_project_metadata,
|
|
11
|
+
git_branch,
|
|
12
|
+
read_version_from_pyproject,
|
|
13
|
+
run,
|
|
14
|
+
sync_metadata_module,
|
|
15
|
+
)
|
|
10
16
|
|
|
11
17
|
__all__ = ["push"]
|
|
12
18
|
|
|
@@ -15,6 +21,7 @@ def push(*, remote: str = "origin", message: str | None = None) -> None:
|
|
|
15
21
|
"""Run checks, commit changes, and push the current branch."""
|
|
16
22
|
|
|
17
23
|
metadata = get_project_metadata()
|
|
24
|
+
sync_metadata_module(metadata)
|
|
18
25
|
version = read_version_from_pyproject(Path("pyproject.toml")) or "unknown"
|
|
19
26
|
click.echo("[push] project diagnostics: " + ", ".join(metadata.diagnostic_lines()))
|
|
20
27
|
click.echo(f"[push] version={version}")
|
|
@@ -7,7 +7,7 @@ import shutil
|
|
|
7
7
|
import subprocess
|
|
8
8
|
import sys
|
|
9
9
|
import tempfile
|
|
10
|
-
from collections.abc import Callable
|
|
10
|
+
from collections.abc import Callable, Sequence
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
from types import ModuleType
|
|
13
13
|
from typing import cast
|
|
@@ -19,6 +19,7 @@ from ._utils import (
|
|
|
19
19
|
bootstrap_dev,
|
|
20
20
|
get_project_metadata,
|
|
21
21
|
run,
|
|
22
|
+
sync_metadata_module,
|
|
22
23
|
)
|
|
23
24
|
|
|
24
25
|
PROJECT = get_project_metadata()
|
|
@@ -51,6 +52,7 @@ def _refresh_default_env() -> None:
|
|
|
51
52
|
def run_coverage(*, verbose: bool = False) -> None:
|
|
52
53
|
"""Run pytest under coverage using python modules to avoid PATH shim issues."""
|
|
53
54
|
|
|
55
|
+
sync_metadata_module(PROJECT)
|
|
54
56
|
bootstrap_dev()
|
|
55
57
|
_prune_coverage_data_files()
|
|
56
58
|
_remove_report_artifacts()
|
|
@@ -95,23 +97,16 @@ def _resolve_pip_audit_ignores() -> list[str]:
|
|
|
95
97
|
def _extract_audit_dependencies(payload: object) -> _AuditPayload:
|
|
96
98
|
"""Normalise `pip-audit --format json` output into dictionaries."""
|
|
97
99
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
if isinstance(payload, dict):
|
|
101
|
-
payload_dict = cast(dict[str, object], payload)
|
|
102
|
-
raw_candidates = payload_dict.get("dependencies", [])
|
|
103
|
-
if isinstance(raw_candidates, list):
|
|
104
|
-
candidate_iter = list(cast(list[object], raw_candidates))
|
|
105
|
-
elif isinstance(payload, list): # pragma: no cover - legacy output
|
|
106
|
-
candidate_iter = list(cast(list[object], payload))
|
|
107
|
-
else: # pragma: no cover - defensive
|
|
108
|
-
return dependencies
|
|
100
|
+
if not isinstance(payload, dict):
|
|
101
|
+
return []
|
|
109
102
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
103
|
+
payload_dict = cast(dict[str, object], payload)
|
|
104
|
+
raw_candidates = payload_dict.get("dependencies", [])
|
|
105
|
+
if not isinstance(raw_candidates, list):
|
|
106
|
+
return []
|
|
113
107
|
|
|
114
|
-
|
|
108
|
+
candidate_list = cast(list[object], raw_candidates)
|
|
109
|
+
return [cast(dict[str, object], candidate) for candidate in candidate_list if isinstance(candidate, dict)]
|
|
115
110
|
|
|
116
111
|
|
|
117
112
|
def run_tests(*, coverage: str = "on", verbose: bool = False, strict_format: bool | None = None) -> None:
|
|
@@ -119,8 +114,10 @@ def run_tests(*, coverage: str = "on", verbose: bool = False, strict_format: boo
|
|
|
119
114
|
if not verbose and env_verbose in _TRUTHY:
|
|
120
115
|
verbose = True
|
|
121
116
|
|
|
117
|
+
sync_metadata_module(PROJECT)
|
|
118
|
+
|
|
122
119
|
def _run(
|
|
123
|
-
cmd:
|
|
120
|
+
cmd: Sequence[str] | str,
|
|
124
121
|
*,
|
|
125
122
|
env: dict[str, str] | None = None,
|
|
126
123
|
check: bool = True,
|
|
@@ -138,7 +135,7 @@ def run_tests(*, coverage: str = "on", verbose: bool = False, strict_format: boo
|
|
|
138
135
|
env_view = " ".join(f"{k}={v}" for k, v in overrides.items())
|
|
139
136
|
click.echo(f" env {env_view}")
|
|
140
137
|
merged_env = _default_env if env is None else _default_env | env
|
|
141
|
-
result = run(cmd, env=merged_env, check=False, capture=capture)
|
|
138
|
+
result = run(cmd, env=merged_env, check=False, capture=capture)
|
|
142
139
|
if verbose and label:
|
|
143
140
|
click.echo(f" -> {label}: exit={result.code} out={bool(result.out)} err={bool(result.err)}")
|
|
144
141
|
|
|
@@ -292,11 +289,8 @@ def run_tests(*, coverage: str = "on", verbose: bool = False, strict_format: boo
|
|
|
292
289
|
)
|
|
293
290
|
|
|
294
291
|
def _run_pytest() -> None:
|
|
295
|
-
for
|
|
296
|
-
|
|
297
|
-
Path(f).unlink()
|
|
298
|
-
except FileNotFoundError:
|
|
299
|
-
pass
|
|
292
|
+
for path in (Path(".coverage"), Path("coverage.xml")):
|
|
293
|
+
path.unlink(missing_ok=True)
|
|
300
294
|
|
|
301
295
|
if coverage == "on" or (coverage == "auto" and (os.getenv("CI") or os.getenv("CODECOV_TOKEN"))):
|
|
302
296
|
click.echo("[coverage] enabled")
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Static package metadata surfaced to CLI commands and documentation.
|
|
2
|
+
|
|
3
|
+
Purpose
|
|
4
|
+
-------
|
|
5
|
+
Expose the current project metadata as simple constants. These values are kept
|
|
6
|
+
in sync with ``pyproject.toml`` by development automation (tests, push
|
|
7
|
+
pipelines), so runtime code does not query packaging metadata.
|
|
8
|
+
|
|
9
|
+
Contents
|
|
10
|
+
--------
|
|
11
|
+
* Module-level constants describing the published package.
|
|
12
|
+
* :func:`print_info` rendering the constants for the CLI ``info`` command.
|
|
13
|
+
|
|
14
|
+
System Role
|
|
15
|
+
-----------
|
|
16
|
+
Lives in the adapters/platform layer; CLI transports import these constants to
|
|
17
|
+
present authoritative project information without invoking packaging APIs.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
#: Distribution name declared in ``pyproject.toml``.
|
|
23
|
+
name = "lib_cli_exit_tools"
|
|
24
|
+
#: Human-readable summary shown in CLI help output.
|
|
25
|
+
title = "CLI exit handling helpers: clean signals, exit codes, and error printing"
|
|
26
|
+
#: Current release version pulled from ``pyproject.toml`` by automation.
|
|
27
|
+
version = "2.1.0"
|
|
28
|
+
#: Repository homepage presented to users.
|
|
29
|
+
homepage = "https://github.com/bitranox/lib_cli_exit_tools"
|
|
30
|
+
#: Author attribution surfaced in CLI output.
|
|
31
|
+
author = "bitranox"
|
|
32
|
+
#: Contact email surfaced in CLI output.
|
|
33
|
+
author_email = "bitranox@gmail.com"
|
|
34
|
+
#: Console-script name published by the package.
|
|
35
|
+
shell_command = "lib-cli-exit-tools"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def print_info() -> None:
|
|
39
|
+
"""Print the summarised metadata block used by the CLI ``info`` command.
|
|
40
|
+
|
|
41
|
+
Why
|
|
42
|
+
Provides a single, auditable rendering function so documentation and
|
|
43
|
+
CLI output always match the system design reference.
|
|
44
|
+
|
|
45
|
+
Side Effects
|
|
46
|
+
Writes to ``stdout``.
|
|
47
|
+
|
|
48
|
+
Examples
|
|
49
|
+
--------
|
|
50
|
+
>>> print_info() # doctest: +ELLIPSIS
|
|
51
|
+
Info for lib_cli_exit_tools:
|
|
52
|
+
...
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
fields = [
|
|
56
|
+
("name", name),
|
|
57
|
+
("title", title),
|
|
58
|
+
("version", version),
|
|
59
|
+
("homepage", homepage),
|
|
60
|
+
("author", author),
|
|
61
|
+
("author_email", author_email),
|
|
62
|
+
("shell_command", shell_command),
|
|
63
|
+
]
|
|
64
|
+
pad = max(len(label) for label, _ in fields)
|
|
65
|
+
lines = [f"Info for {name}:", ""]
|
|
66
|
+
lines.extend(f" {label.ljust(pad)} = {value}" for label, value in fields)
|
|
67
|
+
print("\n".join(lines))
|
|
@@ -136,7 +136,7 @@ def _store_traceback_flag(ctx: click.Context, traceback: bool) -> None:
|
|
|
136
136
|
|
|
137
137
|
@cli.command("info", context_settings=CLICK_CONTEXT_SETTINGS)
|
|
138
138
|
def cli_info() -> None:
|
|
139
|
-
"""Display package metadata
|
|
139
|
+
"""Display package metadata exported by :mod:`lib_cli_exit_tools.__init__conf__`.
|
|
140
140
|
|
|
141
141
|
Why:
|
|
142
142
|
Offer a zero-dependency way for users to confirm the installed version
|
|
@@ -75,6 +75,18 @@ def test_when_info_is_invoked_metadata_is_displayed(cli_runner: CliRunner, strip
|
|
|
75
75
|
assert field in plain_output
|
|
76
76
|
|
|
77
77
|
|
|
78
|
+
@pytest.mark.os_agnostic
|
|
79
|
+
def test_when_version_flag_is_used_metadata_banner_is_returned(
|
|
80
|
+
cli_runner: CliRunner,
|
|
81
|
+
strip_ansi: Callable[[str], str],
|
|
82
|
+
) -> None:
|
|
83
|
+
result: Result = cli_runner.invoke(cli_mod.cli, ["--version"])
|
|
84
|
+
plain_output = strip_ansi(result.output).strip()
|
|
85
|
+
|
|
86
|
+
assert result.exit_code == 0
|
|
87
|
+
assert plain_output == f"{metadata.shell_command} version {metadata.version}"
|
|
88
|
+
|
|
89
|
+
|
|
78
90
|
@pytest.mark.os_agnostic
|
|
79
91
|
def test_when_fail_is_invoked_a_runtime_error_surfaces(cli_runner: CliRunner) -> None:
|
|
80
92
|
result: Result = cli_runner.invoke(cli_mod.cli, ["fail"])
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Metadata stories ensuring generated constants stay authoritative."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from lib_cli_exit_tools import __init__conf__ as metadata
|
|
8
|
+
from scripts import _utils
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@pytest.mark.os_agnostic
|
|
12
|
+
def test_when_print_info_runs_it_lists_every_field(capsys: pytest.CaptureFixture[str]) -> None:
|
|
13
|
+
metadata.print_info()
|
|
14
|
+
|
|
15
|
+
captured = capsys.readouterr().out.splitlines()
|
|
16
|
+
banner, *detail_lines = [line.strip() for line in captured if line.strip()]
|
|
17
|
+
assert banner == f"Info for {metadata.name}:"
|
|
18
|
+
|
|
19
|
+
rows = {}
|
|
20
|
+
for line in detail_lines:
|
|
21
|
+
if " = " not in line:
|
|
22
|
+
continue
|
|
23
|
+
label, value = line.split(" = ", 1)
|
|
24
|
+
rows[label.strip()] = value.strip()
|
|
25
|
+
|
|
26
|
+
assert rows == {
|
|
27
|
+
"name": metadata.name,
|
|
28
|
+
"title": metadata.title,
|
|
29
|
+
"version": metadata.version,
|
|
30
|
+
"homepage": metadata.homepage,
|
|
31
|
+
"author": metadata.author,
|
|
32
|
+
"author_email": metadata.author_email,
|
|
33
|
+
"shell_command": metadata.shell_command,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@pytest.mark.os_agnostic
|
|
38
|
+
def test_metadata_constants_match_pyproject() -> None:
|
|
39
|
+
project = _utils.get_project_metadata()
|
|
40
|
+
|
|
41
|
+
assert metadata.name == project.name
|
|
42
|
+
assert metadata.title == project.summary
|
|
43
|
+
assert metadata.version == project.version
|
|
44
|
+
assert metadata.homepage == project.homepage or project.repo_url or ""
|
|
45
|
+
assert metadata.author == project.author_name
|
|
46
|
+
assert metadata.author_email == project.author_email
|
|
47
|
+
assert metadata.shell_command == project.shell_command
|
|
@@ -1,220 +0,0 @@
|
|
|
1
|
-
"""Metadata facade that keeps CLI help/version text aligned with packaging data.
|
|
2
|
-
|
|
3
|
-
Purpose:
|
|
4
|
-
Expose read-only metadata helpers the CLI can use without importing heavy
|
|
5
|
-
packaging libraries.
|
|
6
|
-
Contents:
|
|
7
|
-
* Metadata lookup helpers prefixed with ``_`` to cache and normalise states.
|
|
8
|
-
* Public constants (`name`, `title`, `version`, etc.) derived from metadata.
|
|
9
|
-
* :func:`print_info` for human-friendly provenance output.
|
|
10
|
-
System Integration:
|
|
11
|
-
Read by :mod:`lib_cli_exit_tools.cli` and documented in
|
|
12
|
-
``docs/system-design/module_reference.md`` so CLI output matches project
|
|
13
|
-
release metadata.
|
|
14
|
-
"""
|
|
15
|
-
|
|
16
|
-
from __future__ import annotations
|
|
17
|
-
|
|
18
|
-
from functools import cache
|
|
19
|
-
from importlib import metadata as _im
|
|
20
|
-
from importlib.metadata import PackageMetadata
|
|
21
|
-
|
|
22
|
-
#: Distribution identifier used for importlib.metadata lookups. Mirrors the
|
|
23
|
-
#: project name declared in ``pyproject.toml`` so metadata resolution stays in
|
|
24
|
-
#: sync with packaging configuration.
|
|
25
|
-
_DIST_NAME = "lib_cli_exit_tools"
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def _get_str(meta: PackageMetadata | None, key: str, default: str = "") -> str:
|
|
29
|
-
"""Why:
|
|
30
|
-
Normalise metadata lookups so CLI output stays predictable.
|
|
31
|
-
What:
|
|
32
|
-
Fetch the string value for ``key`` from ``meta`` or fall back to
|
|
33
|
-
``default`` when the entry is missing or not a plain string.
|
|
34
|
-
Parameters:
|
|
35
|
-
meta: Metadata mapping for the installed distribution, or ``None``
|
|
36
|
-
when running from source.
|
|
37
|
-
key: Field name to fetch (e.g. ``"Summary"``).
|
|
38
|
-
default: Fallback string when the key is absent or unsuitable.
|
|
39
|
-
Returns:
|
|
40
|
-
The resolved string value to display in CLI output.
|
|
41
|
-
Side Effects:
|
|
42
|
-
None.
|
|
43
|
-
"""
|
|
44
|
-
if meta is None:
|
|
45
|
-
return default
|
|
46
|
-
value = meta.get(key)
|
|
47
|
-
return value if isinstance(value, str) else default
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def _meta() -> PackageMetadata | None:
|
|
51
|
-
"""Why:
|
|
52
|
-
Provide a single location that tolerates missing installations.
|
|
53
|
-
What:
|
|
54
|
-
Attempt to read the distribution metadata via ``importlib.metadata``.
|
|
55
|
-
Parameters:
|
|
56
|
-
None.
|
|
57
|
-
Returns:
|
|
58
|
-
Metadata mapping when available, otherwise ``None``.
|
|
59
|
-
Side Effects:
|
|
60
|
-
None.
|
|
61
|
-
"""
|
|
62
|
-
try:
|
|
63
|
-
meta: PackageMetadata = _im.metadata(_DIST_NAME)
|
|
64
|
-
return meta
|
|
65
|
-
except _im.PackageNotFoundError:
|
|
66
|
-
return None
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
def _version() -> str:
|
|
70
|
-
"""Why:
|
|
71
|
-
Surface the installed version for CLI flags and documentation.
|
|
72
|
-
What:
|
|
73
|
-
Retrieve the distribution version, falling back to a development label
|
|
74
|
-
when metadata is unavailable.
|
|
75
|
-
Parameters:
|
|
76
|
-
None.
|
|
77
|
-
Returns:
|
|
78
|
-
Semantic-version string.
|
|
79
|
-
Side Effects:
|
|
80
|
-
None.
|
|
81
|
-
Examples:
|
|
82
|
-
>>> isinstance(_version(), str)
|
|
83
|
-
True
|
|
84
|
-
"""
|
|
85
|
-
try:
|
|
86
|
-
return _im.version(_DIST_NAME)
|
|
87
|
-
except _im.PackageNotFoundError:
|
|
88
|
-
return "0.0.0.dev0"
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
def _home_page(meta: PackageMetadata | None) -> str:
|
|
92
|
-
"""Why:
|
|
93
|
-
Normalise homepage lookups across build backends.
|
|
94
|
-
What:
|
|
95
|
-
Return the documented project homepage, favouring metadata values but
|
|
96
|
-
defaulting to GitHub when absent.
|
|
97
|
-
Parameters:
|
|
98
|
-
meta: Metadata mapping or ``None`` when unavailable.
|
|
99
|
-
Returns:
|
|
100
|
-
Homepage URL string.
|
|
101
|
-
Side Effects:
|
|
102
|
-
None.
|
|
103
|
-
"""
|
|
104
|
-
if meta is None:
|
|
105
|
-
return "https://github.com/bitranox/lib_cli_exit_tools"
|
|
106
|
-
primary = _get_str(meta, "Home-page")
|
|
107
|
-
fallback = _get_str(meta, "Homepage")
|
|
108
|
-
return primary or fallback or "https://github.com/bitranox/lib_cli_exit_tools"
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
def _author(meta: PackageMetadata | None) -> tuple[str, str]:
|
|
112
|
-
"""Why:
|
|
113
|
-
Ensure provenance output always includes a name and contact address.
|
|
114
|
-
What:
|
|
115
|
-
Extract the author name and email from metadata, applying defaults.
|
|
116
|
-
Parameters:
|
|
117
|
-
meta: Metadata mapping or ``None`` when unavailable.
|
|
118
|
-
Returns:
|
|
119
|
-
Tuple of ``(author_name, author_email)``.
|
|
120
|
-
Side Effects:
|
|
121
|
-
None.
|
|
122
|
-
"""
|
|
123
|
-
if meta is None:
|
|
124
|
-
return ("bitranox", "bitranox@gmail.com")
|
|
125
|
-
return (
|
|
126
|
-
_get_str(meta, "Author", "bitranox"),
|
|
127
|
-
_get_str(meta, "Author-email", "bitranox@gmail.com"),
|
|
128
|
-
)
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
def _summary(meta: PackageMetadata | None) -> str:
|
|
132
|
-
"""Why:
|
|
133
|
-
Guarantee a descriptive summary in CLI output even when metadata is sparse.
|
|
134
|
-
What:
|
|
135
|
-
Return the ``Summary`` field or a human-readable default.
|
|
136
|
-
Parameters:
|
|
137
|
-
meta: Metadata mapping or ``None`` when missing.
|
|
138
|
-
Returns:
|
|
139
|
-
Summary string.
|
|
140
|
-
Side Effects:
|
|
141
|
-
None.
|
|
142
|
-
"""
|
|
143
|
-
return _get_str(meta, "Summary", "Functions to exit a CLI application properly")
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
@cache
|
|
147
|
-
def _shell_command() -> str:
|
|
148
|
-
"""Why:
|
|
149
|
-
Surface the console-script name so documentation matches installation.
|
|
150
|
-
What:
|
|
151
|
-
Inspect entry points for the target referencing ``cli:main`` and return
|
|
152
|
-
its advertised name.
|
|
153
|
-
Parameters:
|
|
154
|
-
None.
|
|
155
|
-
Returns:
|
|
156
|
-
Console-script name or the distribution identifier when unresolved.
|
|
157
|
-
Side Effects:
|
|
158
|
-
None.
|
|
159
|
-
Examples:
|
|
160
|
-
>>> isinstance(_shell_command(), str)
|
|
161
|
-
True
|
|
162
|
-
"""
|
|
163
|
-
# Discover console script name mapping to our CLI main, fallback to dist name
|
|
164
|
-
entries = _im.entry_points(group="console_scripts")
|
|
165
|
-
target = "lib_cli_exit_tools.cli:main"
|
|
166
|
-
for entry in entries:
|
|
167
|
-
if entry.value == target:
|
|
168
|
-
return entry.name
|
|
169
|
-
return _DIST_NAME
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
# Public values (resolve metadata once)
|
|
173
|
-
#: Cached metadata mapping to avoid repeated importlib lookups.
|
|
174
|
-
_m = _meta()
|
|
175
|
-
#: Distribution name used when metadata lookups fail.
|
|
176
|
-
name = _DIST_NAME
|
|
177
|
-
#: Human-readable project title displayed in CLI help.
|
|
178
|
-
title = _summary(_m)
|
|
179
|
-
#: Installed package version string surfaced via --version.
|
|
180
|
-
version = _version()
|
|
181
|
-
#: Project homepage for documentation and issue reporting.
|
|
182
|
-
homepage = _home_page(_m)
|
|
183
|
-
#: Primary author details used in info output.
|
|
184
|
-
author, author_email = _author(_m)
|
|
185
|
-
#: Console script entry point that launches this CLI.
|
|
186
|
-
shell_command = _shell_command()
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
def print_info() -> None:
|
|
190
|
-
"""Why:
|
|
191
|
-
Offer operators a quick snapshot of build provenance and support links.
|
|
192
|
-
What:
|
|
193
|
-
Emit a formatted block containing metadata derived from this module.
|
|
194
|
-
Parameters:
|
|
195
|
-
None.
|
|
196
|
-
Returns:
|
|
197
|
-
``None``.
|
|
198
|
-
Side Effects:
|
|
199
|
-
Writes text to ``stdout``.
|
|
200
|
-
Examples:
|
|
201
|
-
>>> import contextlib, io
|
|
202
|
-
>>> buf = io.StringIO()
|
|
203
|
-
>>> with contextlib.redirect_stdout(buf):
|
|
204
|
-
... print_info()
|
|
205
|
-
>>> "Info for" in buf.getvalue()
|
|
206
|
-
True
|
|
207
|
-
"""
|
|
208
|
-
fields = [
|
|
209
|
-
("name", name),
|
|
210
|
-
("title", title),
|
|
211
|
-
("version", version),
|
|
212
|
-
("homepage", homepage),
|
|
213
|
-
("author", author),
|
|
214
|
-
("author_email", author_email),
|
|
215
|
-
("shell_command", shell_command),
|
|
216
|
-
]
|
|
217
|
-
pad = max(len(k) for k, _ in fields)
|
|
218
|
-
lines = [f"Info for {name}:", ""]
|
|
219
|
-
lines += [f" {k.ljust(pad)} = {v}" for k, v in fields]
|
|
220
|
-
print("\n".join(lines))
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
"""Metadata stories ensuring importlib fallbacks stay predictable."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import importlib
|
|
6
|
-
|
|
7
|
-
import pytest
|
|
8
|
-
|
|
9
|
-
from lib_cli_exit_tools import __init__conf__ as metadata
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
@pytest.mark.os_agnostic
|
|
13
|
-
def test_when_print_info_runs_it_lists_every_field(capsys: pytest.CaptureFixture[str]) -> None:
|
|
14
|
-
metadata.print_info()
|
|
15
|
-
|
|
16
|
-
captured = capsys.readouterr().out
|
|
17
|
-
|
|
18
|
-
for label in ("name", "title", "version", "homepage", "author", "author_email", "shell_command"):
|
|
19
|
-
assert label in captured
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
@pytest.mark.os_agnostic
|
|
23
|
-
def test_when_distribution_is_missing_fallback_values_surface(
|
|
24
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
25
|
-
capsys: pytest.CaptureFixture[str],
|
|
26
|
-
) -> None:
|
|
27
|
-
class FailingLookup:
|
|
28
|
-
def __call__(self, _dist: str) -> None:
|
|
29
|
-
raise metadata._im.PackageNotFoundError # type: ignore[attr-defined]
|
|
30
|
-
|
|
31
|
-
with monkeypatch.context() as ctx:
|
|
32
|
-
ctx.setattr(metadata._im, "metadata", FailingLookup()) # type: ignore[attr-defined]
|
|
33
|
-
ctx.setattr(metadata._im, "version", FailingLookup()) # type: ignore[attr-defined]
|
|
34
|
-
reloaded = importlib.reload(metadata)
|
|
35
|
-
|
|
36
|
-
reloaded.print_info()
|
|
37
|
-
|
|
38
|
-
captured = capsys.readouterr().out
|
|
39
|
-
|
|
40
|
-
assert "0.0.0.dev0" in captured
|
|
41
|
-
assert "lib_cli_exit_tools" in captured
|
|
42
|
-
assert "bitranox@gmail.com" in captured
|
|
43
|
-
|
|
44
|
-
importlib.reload(metadata)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
@pytest.mark.os_agnostic
|
|
48
|
-
def test_when_shell_command_is_resolved_matching_entry_point_wins(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
49
|
-
class EntryPoint:
|
|
50
|
-
def __init__(self, name: str, value: str) -> None:
|
|
51
|
-
self.name = name
|
|
52
|
-
self.value = value
|
|
53
|
-
|
|
54
|
-
def fake_entry_points(*, group: str) -> list[EntryPoint]:
|
|
55
|
-
assert group == "console_scripts"
|
|
56
|
-
return [
|
|
57
|
-
EntryPoint("alt-cli", "other.module:main"),
|
|
58
|
-
EntryPoint("lib-cli-exit-tools", "lib_cli_exit_tools.cli:main"),
|
|
59
|
-
]
|
|
60
|
-
|
|
61
|
-
with monkeypatch.context() as ctx:
|
|
62
|
-
ctx.setattr(metadata._im, "entry_points", fake_entry_points) # type: ignore[attr-defined]
|
|
63
|
-
reloaded = importlib.reload(metadata)
|
|
64
|
-
|
|
65
|
-
assert reloaded.shell_command == "lib-cli-exit-tools"
|
|
66
|
-
|
|
67
|
-
importlib.reload(metadata)
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
@pytest.mark.os_agnostic
|
|
71
|
-
def test_when_shell_command_has_no_match_the_distribution_name_is_used(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
72
|
-
def fake_entry_points(*, group: str) -> list[object]:
|
|
73
|
-
assert group == "console_scripts"
|
|
74
|
-
return []
|
|
75
|
-
|
|
76
|
-
with monkeypatch.context() as ctx:
|
|
77
|
-
ctx.setattr(metadata._im, "entry_points", fake_entry_points) # type: ignore[attr-defined]
|
|
78
|
-
reloaded = importlib.reload(metadata)
|
|
79
|
-
assert reloaded.shell_command == "lib_cli_exit_tools"
|
|
80
|
-
|
|
81
|
-
importlib.reload(metadata)
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
@pytest.mark.os_agnostic
|
|
85
|
-
def test_when_metadata_values_are_not_strings_defaults_apply(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
86
|
-
class OddMeta:
|
|
87
|
-
def get(self, key: str, default: object | None = None) -> object:
|
|
88
|
-
return 123
|
|
89
|
-
|
|
90
|
-
def fake_metadata(_dist: str) -> OddMeta:
|
|
91
|
-
return OddMeta()
|
|
92
|
-
|
|
93
|
-
with monkeypatch.context() as ctx:
|
|
94
|
-
ctx.setattr(metadata._im, "metadata", fake_metadata) # type: ignore[attr-defined]
|
|
95
|
-
reloaded = importlib.reload(metadata)
|
|
96
|
-
|
|
97
|
-
assert reloaded.author == "bitranox"
|
|
98
|
-
assert reloaded.author_email == "bitranox@gmail.com"
|
|
99
|
-
assert reloaded.title == "Functions to exit a CLI application properly"
|
|
100
|
-
|
|
101
|
-
importlib.reload(metadata)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/src/lib_cli_exit_tools/adapters/__init__.py
RENAMED
|
File without changes
|
{lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/src/lib_cli_exit_tools/adapters/signals.py
RENAMED
|
File without changes
|
{lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/src/lib_cli_exit_tools/application/__init__.py
RENAMED
|
File without changes
|
{lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/src/lib_cli_exit_tools/application/runner.py
RENAMED
|
File without changes
|
{lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/src/lib_cli_exit_tools/core/__init__.py
RENAMED
|
File without changes
|
{lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/src/lib_cli_exit_tools/core/configuration.py
RENAMED
|
File without changes
|
{lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/src/lib_cli_exit_tools/core/exit_codes.py
RENAMED
|
File without changes
|
{lib_cli_exit_tools-2.0.0 → lib_cli_exit_tools-2.1.0}/src/lib_cli_exit_tools/lib_cli_exit_tools.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|