strictcli 0.19.0__tar.gz → 0.20.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.
- strictcli-0.20.0/.rlsbl/changes/.validated +1 -0
- strictcli-0.20.0/.rlsbl/changes/0.20.0.jsonl +4 -0
- strictcli-0.20.0/.rlsbl/releases/v0.20.0.toml +8 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/CHANGELOG.md +54 -3
- {strictcli-0.19.0 → strictcli-0.20.0}/PKG-INFO +1 -1
- {strictcli-0.19.0 → strictcli-0.20.0}/package-lock.json +2 -2
- {strictcli-0.19.0 → strictcli-0.20.0}/package.json +1 -1
- {strictcli-0.19.0 → strictcli-0.20.0}/pyproject.toml +1 -1
- {strictcli-0.19.0 → strictcli-0.20.0}/strictcli/__init__.py +118 -24
- strictcli-0.20.0/tests/test_command_tags.py +419 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/uv.lock +1 -1
- strictcli-0.19.0/.rlsbl/changes/.validated +0 -1
- {strictcli-0.19.0 → strictcli-0.20.0}/.claude/settings.json +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.github/workflows/ci.yml +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.github/workflows/publish.yml +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.gitignore +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.npmignore +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/bases/.claude/settings.json +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/bases/.github/workflows/ci.yml +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/bases/.github/workflows/publish.yml +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/bases/.gitignore +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/bases/.npmignore +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/bases/.rlsbl/hooks/post-release.sh +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/bases/.rlsbl/hooks/pre-checks.sh +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/bases/.rlsbl/hooks/pre-release.sh +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/bases/.rlsbl/lint/npm.toml +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/bases/.rlsbl/lint/python.toml +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/bases/CHANGELOG.md +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/bases/CLAUDE.md +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/bases/LICENSE +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/changes/0.10.0.jsonl +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/changes/0.10.0.md +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/changes/0.11.0.jsonl +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/changes/0.11.0.md +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/changes/0.12.0.jsonl +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/changes/0.12.0.md +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/changes/0.13.0.jsonl +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/changes/0.13.0.md +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/changes/0.14.0.jsonl +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/changes/0.14.0.md +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/changes/0.15.0.jsonl +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/changes/0.15.0.md +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/changes/0.15.1.jsonl +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/changes/0.15.1.md +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/changes/0.16.0.jsonl +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/changes/0.16.0.md +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/changes/0.16.1.jsonl +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/changes/0.16.1.md +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/changes/0.17.0.jsonl +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/changes/0.17.0.md +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/changes/0.18.0.jsonl +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/changes/0.18.0.md +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/changes/0.19.0.jsonl +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/changes/0.19.0.md +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/changes/0.4.0.jsonl +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/changes/0.4.0.md +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/changes/0.4.1.jsonl +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/changes/0.4.1.md +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/changes/0.5.0.jsonl +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/changes/0.5.0.md +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/changes/0.6.0.jsonl +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/changes/0.6.0.md +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/changes/0.6.1.jsonl +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/changes/0.6.1.md +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/changes/0.7.0.jsonl +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/changes/0.7.0.md +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/changes/0.7.1.jsonl +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/changes/0.7.1.md +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/changes/0.8.0.jsonl +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/changes/0.8.0.md +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/changes/0.8.1.jsonl +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/changes/0.8.1.md +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/changes/0.8.2.jsonl +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/changes/0.8.2.md +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/changes/0.8.3.jsonl +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/changes/0.8.3.md +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/changes/0.8.4.jsonl +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/changes/0.8.4.md +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/changes/0.8.5.jsonl +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/changes/0.8.5.md +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/changes/0.8.6.jsonl +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/changes/0.8.6.md +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/changes/0.8.7.jsonl +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/changes/0.8.7.md +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/changes/0.9.0.jsonl +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/changes/0.9.0.md +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/changes/0.9.1.jsonl +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/changes/0.9.1.md +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/changes/unreleased.jsonl +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/config.json +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/hashes.json +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/hooks/post-release.sh +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/hooks/pre-checks.sh +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/hooks/pre-release.sh +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/lint/npm.toml +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/lint/python.toml +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/releases/unreleased.toml +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/releases/v0.10.0.toml +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/releases/v0.11.0.toml +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/releases/v0.12.0.toml +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/releases/v0.13.0.toml +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/releases/v0.15.0.toml +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/releases/v0.15.1.toml +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/releases/v0.16.0.toml +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/releases/v0.16.1.toml +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/releases/v0.17.0.toml +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/releases/v0.18.0.toml +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/releases/v0.19.0.toml +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/releases/v0.8.5.toml +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/releases/v0.8.6.toml +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/releases/v0.8.7.toml +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/releases/v0.9.0.toml +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/releases/v0.9.1.toml +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.rlsbl/version +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/.strictcli/schema.json +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/CLAUDE.md +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/LICENSE +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/README.md +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/index.js +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/postinstall.js +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/tests/test_arg_default.py +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/tests/test_at_prefix.py +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/tests/test_auto_version.py +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/tests/test_check_command.py +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/tests/test_check_discovery.py +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/tests/test_check_public_api.py +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/tests/test_check_runner.py +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/tests/test_check_schema.py +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/tests/test_check_types.py +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/tests/test_choices.py +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/tests/test_command_help_suggestion.py +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/tests/test_config.py +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/tests/test_config_file_path.py +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/tests/test_config_set_bugs.py +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/tests/test_deep_nesting.py +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/tests/test_dependencies.py +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/tests/test_deprecated.py +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/tests/test_dump_schema.py +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/tests/test_e2e.py +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/tests/test_env.py +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/tests/test_exit_codes.py +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/tests/test_flag_sets.py +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/tests/test_float_type.py +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/tests/test_global_flags.py +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/tests/test_help.py +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/tests/test_int_type.py +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/tests/test_mutex.py +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/tests/test_nesting.py +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/tests/test_parser.py +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/tests/test_passthrough.py +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/tests/test_registration.py +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/tests/test_repeatable.py +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/tests/test_tagdsl.py +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/tests/test_toml_loading.py +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/tests/test_unique.py +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/tests/test_utilities.py +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/tests/test_validate.py +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/tests/test_variadic.py +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/todo/.defer/deferred.md +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/todo/.done/original-idea.md +0 -0
- {strictcli-0.19.0 → strictcli-0.20.0}/todo/.done/public-check-runner-api.md +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
019cae063cca2c44324d0c76260c72e72334b7c8
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
{"commits":["9e7e116531923a9db84c2f9e7fc6326a9bf01079","a68465be720ee0728e72a09f5ba6bd3d563a9600"],"user_facing":true,"description":"**Breaking.** Command dataclass is now `frozen=True` with `tuple` fields instead of `list`.","type":"breaking"}
|
|
2
|
+
{"commits":["c5009427ea363e04d8c0096a1ec0628547317866","7808c1c5b08272af346aa4d0e9d69ff3ccef8c78"],"user_facing":true,"description":"**New.** Command tags: string labels on commands and groups with group-to-command inheritance.","type":"feature"}
|
|
3
|
+
{"commits":["ef7a4711dbcf7c0a53a3666de4507aab7c1cf9e0"],"user_facing":true,"description":"**New.** Tag contracts: registration-time validation that tagged commands have required flags.","type":"feature"}
|
|
4
|
+
{"commits":["bc911b9134605042b454bc2192c3fe68ee6f92bf","ff5ff5aa65b216fec2788f942751f11a8ca8b243"],"user_facing":false}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Version bump type: patch, minor, or major
|
|
2
|
+
bump = "minor"
|
|
3
|
+
# Short description of this release (required)
|
|
4
|
+
description = "Frozen Command, command tags, tag contracts"
|
|
5
|
+
# Optional context explaining why these changes were made
|
|
6
|
+
context = "Breaking: Command dataclass is now frozen=True with tuple fields (was mutable list). New: command tags (string labels with group inheritance) and tag contracts (registration-time validation that tagged commands have required flags)."
|
|
7
|
+
include = ["pypi", "npm"]
|
|
8
|
+
exclude = []
|
|
@@ -2,29 +2,51 @@
|
|
|
2
2
|
|
|
3
3
|
# Changelog
|
|
4
4
|
|
|
5
|
-
## 0.
|
|
5
|
+
## 0.20.0
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Frozen Command, command tags, tag contracts
|
|
8
8
|
|
|
9
9
|
<details>
|
|
10
10
|
<summary>Context</summary>
|
|
11
11
|
|
|
12
|
-
Breaking:
|
|
12
|
+
Breaking: Command dataclass is now frozen=True with tuple fields (was mutable list). New: command tags (string labels with group inheritance) and tag contracts (registration-time validation that tagged commands have required flags).
|
|
13
13
|
|
|
14
14
|
</details>
|
|
15
15
|
|
|
16
16
|
### Breaking
|
|
17
17
|
|
|
18
|
+
- **Breaking.** Command dataclass is now `frozen=True` with `tuple` fields instead of `list`.
|
|
19
|
+
|
|
20
|
+
### Features
|
|
21
|
+
|
|
22
|
+
- **New.** Command tags: string labels on commands and groups with group-to-command inheritance.
|
|
23
|
+
- **New.** Tag contracts: registration-time validation that tagged commands have required flags.
|
|
24
|
+
|
|
25
|
+
## 0.19.0
|
|
26
|
+
|
|
27
|
+
### Breaking
|
|
28
|
+
|
|
18
29
|
- **Breaking.** Renamed `Tag` to `FlagSet` and `tags=` parameter to `flag_sets=` across all API surfaces.
|
|
19
30
|
|
|
20
31
|
## 0.18.0
|
|
21
32
|
|
|
33
|
+
Public API for running checks programmatically
|
|
34
|
+
|
|
35
|
+
<details>
|
|
36
|
+
<summary>Context</summary>
|
|
37
|
+
|
|
38
|
+
Adds App.run_checks(), format_check_results(), format_check_results_json(), and CheckRunResult — a stable public API replacing the private _filter_checks/_resolve_check_order/_run_checks/_check_format_human functions that consumers like rlsbl were forced to use.
|
|
39
|
+
|
|
40
|
+
</details>
|
|
41
|
+
|
|
22
42
|
### Features
|
|
23
43
|
|
|
24
44
|
- **New.** Public API for running checks programmatically: `App.run_checks()`, `format_check_results()`, `format_check_results_json()`, `CheckRunResult`.
|
|
25
45
|
|
|
26
46
|
## 0.17.0
|
|
27
47
|
|
|
48
|
+
Repeatable flag default validation and help text display
|
|
49
|
+
|
|
28
50
|
### Breaking
|
|
29
51
|
|
|
30
52
|
- **Breaking.** Repeatable flag defaults are now validated at registration: must be a list, `default=[]` is rejected as redundant, and element types must match the flag type.
|
|
@@ -35,6 +57,8 @@ Breaking: Tag class renamed to FlagSet, tags= parameter renamed to flag_sets= ac
|
|
|
35
57
|
|
|
36
58
|
## 0.16.1
|
|
37
59
|
|
|
60
|
+
Error message fixes and help text improvements
|
|
61
|
+
|
|
38
62
|
### Fixes
|
|
39
63
|
|
|
40
64
|
- **Fix.** `config set` mutex error messages now include the `config set:` prefix for consistency.
|
|
@@ -42,6 +66,15 @@ Breaking: Tag class renamed to FlagSet, tags= parameter renamed to flag_sets= ac
|
|
|
42
66
|
|
|
43
67
|
## 0.16.0
|
|
44
68
|
|
|
69
|
+
unique and env_separator flag fields, config array coercion, config set repeatable support
|
|
70
|
+
|
|
71
|
+
<details>
|
|
72
|
+
<summary>Context</summary>
|
|
73
|
+
|
|
74
|
+
Breaking: config show now requires --plain or --json. New features: unique field enforces no duplicate values on repeatable flags, env_separator controls how env vars are split into arrays, config arrays are coerced to declared types, config set supports repeatable flags with --clear and --default.
|
|
75
|
+
|
|
76
|
+
</details>
|
|
77
|
+
|
|
45
78
|
### Breaking
|
|
46
79
|
|
|
47
80
|
- **Config show.** `config show` now requires explicit `--plain` or `--json` flag instead of defaulting to plain text.
|
|
@@ -61,6 +94,15 @@ Breaking: Tag class renamed to FlagSet, tags= parameter renamed to flag_sets= ac
|
|
|
61
94
|
|
|
62
95
|
## 0.15.1
|
|
63
96
|
|
|
97
|
+
Config show bool parity and negative values in config set
|
|
98
|
+
|
|
99
|
+
<details>
|
|
100
|
+
<summary>Context</summary>
|
|
101
|
+
|
|
102
|
+
config show now outputs lowercase true/false matching Go. config set accepts negative numeric values.
|
|
103
|
+
|
|
104
|
+
</details>
|
|
105
|
+
|
|
64
106
|
### Breaking
|
|
65
107
|
|
|
66
108
|
- **Breaking.** Config set now accepts negative numeric values. Unknown single-dash tokens produce 'unexpected argument' instead of 'unknown flag'.
|
|
@@ -71,6 +113,15 @@ Breaking: Tag class renamed to FlagSet, tags= parameter renamed to flag_sets= ac
|
|
|
71
113
|
|
|
72
114
|
## 0.15.0
|
|
73
115
|
|
|
116
|
+
Config set type coercion and key validation
|
|
117
|
+
|
|
118
|
+
<details>
|
|
119
|
+
<summary>Context</summary>
|
|
120
|
+
|
|
121
|
+
config set now validates keys against registered flags and coerces string values to the flag's declared type before writing. This is a breaking change: unknown keys are rejected.
|
|
122
|
+
|
|
123
|
+
</details>
|
|
124
|
+
|
|
74
125
|
### Breaking
|
|
75
126
|
|
|
76
127
|
- **Breaking.** `config set` now validates keys against registered flags and rejects unknown keys.
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "strictcli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "strictcli",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.20.0",
|
|
10
10
|
"hasInstallScript": true,
|
|
11
11
|
"license": "MIT"
|
|
12
12
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
__version__ = "0.
|
|
5
|
+
__version__ = "0.20.0"
|
|
6
6
|
|
|
7
7
|
__all__ = [
|
|
8
8
|
"App", "Flag", "Arg", "FlagSet", "MutexGroup", "CoRequired", "Requires",
|
|
@@ -252,7 +252,7 @@ def _config_typename(value: object) -> str:
|
|
|
252
252
|
return type(value).__name__
|
|
253
253
|
|
|
254
254
|
|
|
255
|
-
|
|
255
|
+
_IDENTIFIER_RE = re.compile(r"^[a-z][a-z0-9-]*$")
|
|
256
256
|
_CHECK_REQUIRED_FIELDS = {"tags", "severity", "fast", "pure", "needs_network", "depends_on"}
|
|
257
257
|
_CHECK_VALID_SEVERITIES = {"error", "warn"}
|
|
258
258
|
|
|
@@ -290,7 +290,7 @@ def _parse_checks_toml(data: bytes) -> tuple[str, dict[str, _CheckDef]]:
|
|
|
290
290
|
|
|
291
291
|
for name, fields in checks_section.items():
|
|
292
292
|
# Validate check name
|
|
293
|
-
if not
|
|
293
|
+
if not _IDENTIFIER_RE.match(name):
|
|
294
294
|
raise ValueError(
|
|
295
295
|
f'checks.toml: invalid check name "{name}" '
|
|
296
296
|
f"(must match [a-z][a-z0-9-]*)"
|
|
@@ -731,22 +731,26 @@ class DeprecatedCommand:
|
|
|
731
731
|
message: str
|
|
732
732
|
|
|
733
733
|
|
|
734
|
-
@dataclass
|
|
734
|
+
@dataclass(frozen=True)
|
|
735
735
|
class Command:
|
|
736
736
|
"""A leaf command with a handler."""
|
|
737
737
|
|
|
738
738
|
name: str
|
|
739
739
|
help: str
|
|
740
740
|
handler: Callable | None
|
|
741
|
-
flags:
|
|
742
|
-
args:
|
|
743
|
-
flag_sets:
|
|
744
|
-
mutex:
|
|
745
|
-
dependencies:
|
|
741
|
+
flags: tuple[Flag, ...] = ()
|
|
742
|
+
args: tuple[Arg, ...] = ()
|
|
743
|
+
flag_sets: tuple[FlagSet, ...] = ()
|
|
744
|
+
mutex: tuple[MutexGroup, ...] = ()
|
|
745
|
+
dependencies: tuple[CoRequired | Requires | Implies, ...] = ()
|
|
746
746
|
passthrough: Passthrough | None = None
|
|
747
|
+
tags: frozenset[str] = frozenset()
|
|
747
748
|
|
|
748
749
|
def __post_init__(self) -> None:
|
|
749
750
|
_require_non_empty_str(self.help, "help", "Command")
|
|
751
|
+
for tag in self.tags:
|
|
752
|
+
if not _IDENTIFIER_RE.match(tag):
|
|
753
|
+
raise ValueError(f'invalid tag name "{tag}": must match [a-z][a-z0-9-]*')
|
|
750
754
|
|
|
751
755
|
|
|
752
756
|
@dataclass
|
|
@@ -760,11 +764,16 @@ class Group:
|
|
|
760
764
|
deprecated: dict[str, DeprecatedCommand] = field(default_factory=dict)
|
|
761
765
|
env_prefix: str | None = None
|
|
762
766
|
_global_flags: list[Flag] = field(default_factory=list)
|
|
767
|
+
tags: frozenset[str] = frozenset()
|
|
768
|
+
_accumulated_tags: frozenset[str] = frozenset()
|
|
763
769
|
|
|
764
770
|
def __post_init__(self) -> None:
|
|
765
771
|
_require_non_empty_str(self.help, "help", "Group")
|
|
772
|
+
for tag in self.tags:
|
|
773
|
+
if not _IDENTIFIER_RE.match(tag):
|
|
774
|
+
raise ValueError(f'invalid tag name "{tag}": must match [a-z][a-z0-9-]*')
|
|
766
775
|
|
|
767
|
-
def group(self, name: str, *, help: str) -> Group:
|
|
776
|
+
def group(self, name: str, *, help: str, tags: set[str] | None = None) -> Group:
|
|
768
777
|
"""Create and register a child subgroup."""
|
|
769
778
|
if name in self.commands:
|
|
770
779
|
raise ValueError(
|
|
@@ -774,8 +783,11 @@ class Group:
|
|
|
774
783
|
raise ValueError(
|
|
775
784
|
f'group "{name}" is already registered'
|
|
776
785
|
)
|
|
786
|
+
own_tags = frozenset(tags or set())
|
|
777
787
|
grp = Group(name=name, help=help, env_prefix=self.env_prefix,
|
|
778
|
-
_global_flags=self._global_flags
|
|
788
|
+
_global_flags=self._global_flags,
|
|
789
|
+
tags=own_tags,
|
|
790
|
+
_accumulated_tags=self._accumulated_tags | own_tags)
|
|
779
791
|
self._groups[name] = grp
|
|
780
792
|
return grp
|
|
781
793
|
|
|
@@ -809,6 +821,7 @@ class Group:
|
|
|
809
821
|
mutex: list[MutexGroup] | None = None,
|
|
810
822
|
dependencies: list[CoRequired | Requires | Implies] | None = None,
|
|
811
823
|
passthrough: Passthrough | None = None,
|
|
824
|
+
tags: set[str] | None = None,
|
|
812
825
|
) -> Callable:
|
|
813
826
|
"""Decorator to register a command within this group."""
|
|
814
827
|
|
|
@@ -823,6 +836,8 @@ class Group:
|
|
|
823
836
|
env_prefix=self.env_prefix,
|
|
824
837
|
global_flags=self._global_flags,
|
|
825
838
|
passthrough=passthrough,
|
|
839
|
+
tags=tags,
|
|
840
|
+
inherited_tags=self._accumulated_tags,
|
|
826
841
|
)
|
|
827
842
|
self.commands[name] = cmd
|
|
828
843
|
return func
|
|
@@ -961,6 +976,8 @@ class App:
|
|
|
961
976
|
self._check_defs: dict[str, _CheckDef] = {}
|
|
962
977
|
self._checks_enabled = False
|
|
963
978
|
|
|
979
|
+
self._tag_contracts: dict[str, str] = {}
|
|
980
|
+
|
|
964
981
|
@property
|
|
965
982
|
def config_file_path(self) -> str:
|
|
966
983
|
"""Return the resolved config file path for this app."""
|
|
@@ -1003,6 +1020,50 @@ class App:
|
|
|
1003
1020
|
)
|
|
1004
1021
|
return None
|
|
1005
1022
|
|
|
1023
|
+
def tag_contract(self, tag: str, *, requires_flag: str) -> None:
|
|
1024
|
+
"""Declare that any command with the given tag must have the named flag."""
|
|
1025
|
+
if not _IDENTIFIER_RE.match(tag):
|
|
1026
|
+
raise ValueError(f'invalid tag name "{tag}": must match [a-z][a-z0-9-]*')
|
|
1027
|
+
self._tag_contracts[tag] = requires_flag
|
|
1028
|
+
|
|
1029
|
+
def _validate_tag_contracts(self) -> str | None:
|
|
1030
|
+
"""Check that all tag contracts are satisfied.
|
|
1031
|
+
|
|
1032
|
+
Returns an error message if any command violates a contract, or None.
|
|
1033
|
+
"""
|
|
1034
|
+
if not self._tag_contracts:
|
|
1035
|
+
return None
|
|
1036
|
+
|
|
1037
|
+
def _check_commands(commands: dict) -> str | None:
|
|
1038
|
+
for cmd in commands.values():
|
|
1039
|
+
if cmd.passthrough is not None:
|
|
1040
|
+
continue
|
|
1041
|
+
for tag in cmd.tags:
|
|
1042
|
+
if tag in self._tag_contracts:
|
|
1043
|
+
required_flag = self._tag_contracts[tag]
|
|
1044
|
+
flag_names = {f.name for f in cmd.flags}
|
|
1045
|
+
if required_flag not in flag_names:
|
|
1046
|
+
return (
|
|
1047
|
+
f'command "{cmd.name}": tag "{tag}" requires '
|
|
1048
|
+
f'flag "--{required_flag}"'
|
|
1049
|
+
)
|
|
1050
|
+
return None
|
|
1051
|
+
|
|
1052
|
+
def _check_groups(groups: dict) -> str | None:
|
|
1053
|
+
for group in groups.values():
|
|
1054
|
+
err = _check_commands(group.commands)
|
|
1055
|
+
if err:
|
|
1056
|
+
return err
|
|
1057
|
+
err = _check_groups(group._groups)
|
|
1058
|
+
if err:
|
|
1059
|
+
return err
|
|
1060
|
+
return None
|
|
1061
|
+
|
|
1062
|
+
err = _check_commands(self._commands)
|
|
1063
|
+
if err:
|
|
1064
|
+
return err
|
|
1065
|
+
return _check_groups(self._groups)
|
|
1066
|
+
|
|
1006
1067
|
def set_check_context(self, factory: Callable) -> None:
|
|
1007
1068
|
"""Set the factory function that creates CheckContext for check runs.
|
|
1008
1069
|
|
|
@@ -1144,6 +1205,7 @@ class App:
|
|
|
1144
1205
|
mutex: list[MutexGroup] | None = None,
|
|
1145
1206
|
dependencies: list[CoRequired | Requires | Implies] | None = None,
|
|
1146
1207
|
passthrough: Passthrough | None = None,
|
|
1208
|
+
tags: set[str] | None = None,
|
|
1147
1209
|
) -> Callable:
|
|
1148
1210
|
"""Decorator to register a top-level command."""
|
|
1149
1211
|
|
|
@@ -1159,16 +1221,21 @@ class App:
|
|
|
1159
1221
|
env_prefix=self.env_prefix,
|
|
1160
1222
|
global_flags=self._global_flags,
|
|
1161
1223
|
passthrough=passthrough,
|
|
1224
|
+
tags=tags,
|
|
1225
|
+
inherited_tags=None,
|
|
1162
1226
|
)
|
|
1163
1227
|
self._commands[name] = cmd
|
|
1164
1228
|
return func
|
|
1165
1229
|
|
|
1166
1230
|
return decorator
|
|
1167
1231
|
|
|
1168
|
-
def group(self, name: str, *, help: str) -> Group:
|
|
1232
|
+
def group(self, name: str, *, help: str, tags: set[str] | None = None) -> Group:
|
|
1169
1233
|
"""Create and register a command group."""
|
|
1234
|
+
own_tags = frozenset(tags or set())
|
|
1170
1235
|
grp = Group(name=name, help=help, env_prefix=self.env_prefix,
|
|
1171
|
-
_global_flags=self._global_flags
|
|
1236
|
+
_global_flags=self._global_flags,
|
|
1237
|
+
tags=own_tags,
|
|
1238
|
+
_accumulated_tags=own_tags)
|
|
1172
1239
|
self._groups[name] = grp
|
|
1173
1240
|
return grp
|
|
1174
1241
|
|
|
@@ -1288,8 +1355,8 @@ class App:
|
|
|
1288
1355
|
name="show",
|
|
1289
1356
|
help="Show all config values with source attribution",
|
|
1290
1357
|
handler=_config_show_handler,
|
|
1291
|
-
flags=config_show_flags,
|
|
1292
|
-
mutex=config_show_mutex,
|
|
1358
|
+
flags=tuple(config_show_flags),
|
|
1359
|
+
mutex=tuple(config_show_mutex),
|
|
1293
1360
|
)
|
|
1294
1361
|
|
|
1295
1362
|
# config set
|
|
@@ -1440,18 +1507,18 @@ class App:
|
|
|
1440
1507
|
name="set",
|
|
1441
1508
|
help="Set a config value",
|
|
1442
1509
|
handler=_config_set_handler,
|
|
1443
|
-
args=
|
|
1510
|
+
args=(
|
|
1444
1511
|
Arg(name="key", help="Config key to set"),
|
|
1445
1512
|
Arg(name="value",
|
|
1446
1513
|
help="Value to set (comma-separated for repeatable flags, use backslash to escape commas)",
|
|
1447
1514
|
required=False),
|
|
1448
|
-
|
|
1449
|
-
flags=
|
|
1515
|
+
),
|
|
1516
|
+
flags=(
|
|
1450
1517
|
Flag(name="clear", type=bool,
|
|
1451
1518
|
help="Clear a repeatable flag (set to empty list)"),
|
|
1452
1519
|
Flag(name="default", type=bool,
|
|
1453
1520
|
help="Reset a key to its default (remove from config)"),
|
|
1454
|
-
|
|
1521
|
+
),
|
|
1455
1522
|
)
|
|
1456
1523
|
|
|
1457
1524
|
# config edit
|
|
@@ -1924,6 +1991,10 @@ class App:
|
|
|
1924
1991
|
if check_err:
|
|
1925
1992
|
print(f"error: {check_err}", file=sys.stderr)
|
|
1926
1993
|
sys.exit(1)
|
|
1994
|
+
tag_err = self._validate_tag_contracts()
|
|
1995
|
+
if tag_err:
|
|
1996
|
+
print(f"error: {tag_err}", file=sys.stderr)
|
|
1997
|
+
sys.exit(1)
|
|
1927
1998
|
argv = sys.argv[1:]
|
|
1928
1999
|
try:
|
|
1929
2000
|
cmd, data = self._parse(argv)
|
|
@@ -1974,6 +2045,15 @@ class App:
|
|
|
1974
2045
|
exit_code=1,
|
|
1975
2046
|
)
|
|
1976
2047
|
|
|
2048
|
+
tag_err = self._validate_tag_contracts()
|
|
2049
|
+
if tag_err:
|
|
2050
|
+
stderr_buf.write(f"error: {tag_err}\n")
|
|
2051
|
+
return Result(
|
|
2052
|
+
stdout=stdout_buf.getvalue(),
|
|
2053
|
+
stderr=stderr_buf.getvalue(),
|
|
2054
|
+
exit_code=1,
|
|
2055
|
+
)
|
|
2056
|
+
|
|
1977
2057
|
try:
|
|
1978
2058
|
cmd, data = self._parse(argv)
|
|
1979
2059
|
except _HelpRequested as e:
|
|
@@ -2490,11 +2570,15 @@ def _build_and_validate_command(
|
|
|
2490
2570
|
global_flags: list[Flag] | None = None,
|
|
2491
2571
|
passthrough: Passthrough | None = None,
|
|
2492
2572
|
extra_flags: list[Flag] | None = None,
|
|
2573
|
+
tags: set[str] | None = None,
|
|
2574
|
+
inherited_tags: frozenset[str] | None = None,
|
|
2493
2575
|
) -> Command:
|
|
2494
2576
|
"""Build a Command from a decorated handler, validate everything."""
|
|
2495
2577
|
if not help or not help.strip():
|
|
2496
2578
|
raise ValueError(f'command "{name}": missing help text')
|
|
2497
2579
|
|
|
2580
|
+
effective_tags = (inherited_tags or frozenset()) | frozenset(tags or set())
|
|
2581
|
+
|
|
2498
2582
|
# Passthrough commands must not have flags, args, flag sets, or mutex groups
|
|
2499
2583
|
if passthrough is not None:
|
|
2500
2584
|
decorator_flags = list(getattr(handler, "_strictcli_flags", []))
|
|
@@ -2522,6 +2606,7 @@ def _build_and_validate_command(
|
|
|
2522
2606
|
help=help,
|
|
2523
2607
|
handler=None,
|
|
2524
2608
|
passthrough=passthrough,
|
|
2609
|
+
tags=effective_tags,
|
|
2525
2610
|
)
|
|
2526
2611
|
|
|
2527
2612
|
# Collect flags attached by @strictcli.flag decorators
|
|
@@ -2743,11 +2828,12 @@ def _build_and_validate_command(
|
|
|
2743
2828
|
name=name,
|
|
2744
2829
|
help=help,
|
|
2745
2830
|
handler=handler,
|
|
2746
|
-
flags=all_flags,
|
|
2747
|
-
args=all_args,
|
|
2748
|
-
flag_sets=resolved_flag_sets,
|
|
2749
|
-
mutex=resolved_mutex,
|
|
2750
|
-
dependencies=resolved_dependencies,
|
|
2831
|
+
flags=tuple(all_flags),
|
|
2832
|
+
args=tuple(all_args),
|
|
2833
|
+
flag_sets=tuple(resolved_flag_sets),
|
|
2834
|
+
mutex=tuple(resolved_mutex),
|
|
2835
|
+
dependencies=tuple(resolved_dependencies),
|
|
2836
|
+
tags=effective_tags,
|
|
2751
2837
|
)
|
|
2752
2838
|
|
|
2753
2839
|
|
|
@@ -3598,6 +3684,9 @@ def _serialize_command(cmd: Command) -> dict:
|
|
|
3598
3684
|
args = [_serialize_arg(a) for a in cmd.args]
|
|
3599
3685
|
if args:
|
|
3600
3686
|
d["args"] = args
|
|
3687
|
+
tags = sorted(cmd.tags)
|
|
3688
|
+
if tags:
|
|
3689
|
+
d["tags"] = tags
|
|
3601
3690
|
return d
|
|
3602
3691
|
|
|
3603
3692
|
|
|
@@ -3620,6 +3709,9 @@ def _serialize_group(group: Group) -> dict:
|
|
|
3620
3709
|
deprecated = {name: dep.message for name, dep in group.deprecated.items()}
|
|
3621
3710
|
if deprecated:
|
|
3622
3711
|
d["deprecated"] = deprecated
|
|
3712
|
+
tags = sorted(group.tags)
|
|
3713
|
+
if tags:
|
|
3714
|
+
d["tags"] = tags
|
|
3623
3715
|
return d
|
|
3624
3716
|
|
|
3625
3717
|
|
|
@@ -3653,11 +3745,13 @@ def _build_schema_defaults() -> dict:
|
|
|
3653
3745
|
"passthrough": False,
|
|
3654
3746
|
"flags": [],
|
|
3655
3747
|
"args": [],
|
|
3748
|
+
"tags": [],
|
|
3656
3749
|
},
|
|
3657
3750
|
"group": {
|
|
3658
3751
|
"commands": {},
|
|
3659
3752
|
"groups": {},
|
|
3660
3753
|
"deprecated": {},
|
|
3754
|
+
"tags": [],
|
|
3661
3755
|
},
|
|
3662
3756
|
}
|
|
3663
3757
|
|