strictcli 0.17.0__tar.gz → 0.19.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.19.0/.rlsbl/changes/.validated +1 -0
- strictcli-0.19.0/.rlsbl/changes/0.17.0.md +9 -0
- strictcli-0.19.0/.rlsbl/changes/0.18.0.jsonl +4 -0
- strictcli-0.19.0/.rlsbl/changes/0.18.0.md +5 -0
- strictcli-0.19.0/.rlsbl/changes/0.19.0.jsonl +2 -0
- strictcli-0.19.0/.rlsbl/changes/0.19.0.md +5 -0
- strictcli-0.19.0/.rlsbl/releases/v0.18.0.toml +8 -0
- strictcli-0.19.0/.rlsbl/releases/v0.19.0.toml +0 -0
- strictcli-0.19.0/.rlsbl/version +1 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/CHANGELOG.md +21 -2
- {strictcli-0.17.0 → strictcli-0.19.0}/PKG-INFO +7 -7
- {strictcli-0.17.0 → strictcli-0.19.0}/README.md +6 -6
- {strictcli-0.17.0 → strictcli-0.19.0}/package-lock.json +2 -2
- {strictcli-0.17.0 → strictcli-0.19.0}/package.json +1 -1
- {strictcli-0.17.0 → strictcli-0.19.0}/pyproject.toml +1 -1
- {strictcli-0.17.0 → strictcli-0.19.0}/strictcli/__init__.py +101 -52
- strictcli-0.19.0/tests/test_check_public_api.py +435 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/tests/test_check_runner.py +9 -9
- {strictcli-0.17.0 → strictcli-0.19.0}/tests/test_e2e.py +8 -8
- strictcli-0.17.0/tests/test_tags.py → strictcli-0.19.0/tests/test_flag_sets.py +22 -22
- {strictcli-0.17.0 → strictcli-0.19.0}/tests/test_passthrough.py +5 -5
- {strictcli-0.17.0 → strictcli-0.19.0}/tests/test_registration.py +4 -4
- strictcli-0.19.0/todo/.done/public-check-runner-api.md +36 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/uv.lock +1 -1
- strictcli-0.17.0/.rlsbl/bases/.rlsbl/lint/go.toml +0 -17
- strictcli-0.17.0/.rlsbl/changes/.validated +0 -1
- strictcli-0.17.0/.rlsbl/lint/go.toml +0 -17
- strictcli-0.17.0/.rlsbl/version +0 -1
- {strictcli-0.17.0 → strictcli-0.19.0}/.claude/settings.json +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.github/workflows/ci.yml +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.github/workflows/publish.yml +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.gitignore +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.npmignore +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/bases/.claude/settings.json +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/bases/.github/workflows/ci.yml +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/bases/.github/workflows/publish.yml +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/bases/.gitignore +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/bases/.npmignore +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/bases/.rlsbl/hooks/post-release.sh +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/bases/.rlsbl/hooks/pre-checks.sh +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/bases/.rlsbl/hooks/pre-release.sh +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/bases/.rlsbl/lint/npm.toml +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/bases/.rlsbl/lint/python.toml +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/bases/CHANGELOG.md +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/bases/CLAUDE.md +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/bases/LICENSE +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.10.0.jsonl +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.10.0.md +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.11.0.jsonl +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.11.0.md +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.12.0.jsonl +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.12.0.md +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.13.0.jsonl +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.13.0.md +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.14.0.jsonl +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.14.0.md +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.15.0.jsonl +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.15.0.md +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.15.1.jsonl +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.15.1.md +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.16.0.jsonl +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.16.0.md +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.16.1.jsonl +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.16.1.md +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.17.0.jsonl +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.4.0.jsonl +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.4.0.md +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.4.1.jsonl +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.4.1.md +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.5.0.jsonl +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.5.0.md +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.6.0.jsonl +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.6.0.md +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.6.1.jsonl +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.6.1.md +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.7.0.jsonl +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.7.0.md +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.7.1.jsonl +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.7.1.md +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.8.0.jsonl +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.8.0.md +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.8.1.jsonl +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.8.1.md +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.8.2.jsonl +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.8.2.md +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.8.3.jsonl +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.8.3.md +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.8.4.jsonl +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.8.4.md +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.8.5.jsonl +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.8.5.md +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.8.6.jsonl +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.8.6.md +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.8.7.jsonl +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.8.7.md +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.9.0.jsonl +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.9.0.md +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.9.1.jsonl +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.9.1.md +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/unreleased.jsonl +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/config.json +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/hashes.json +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/hooks/post-release.sh +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/hooks/pre-checks.sh +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/hooks/pre-release.sh +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/lint/npm.toml +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/lint/python.toml +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/releases/unreleased.toml +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/releases/v0.10.0.toml +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/releases/v0.11.0.toml +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/releases/v0.12.0.toml +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/releases/v0.13.0.toml +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/releases/v0.15.0.toml +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/releases/v0.15.1.toml +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/releases/v0.16.0.toml +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/releases/v0.16.1.toml +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/releases/v0.17.0.toml +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/releases/v0.8.5.toml +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/releases/v0.8.6.toml +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/releases/v0.8.7.toml +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/releases/v0.9.0.toml +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/releases/v0.9.1.toml +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/.strictcli/schema.json +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/CLAUDE.md +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/LICENSE +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/index.js +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/postinstall.js +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/tests/test_arg_default.py +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/tests/test_at_prefix.py +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/tests/test_auto_version.py +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/tests/test_check_command.py +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/tests/test_check_discovery.py +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/tests/test_check_schema.py +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/tests/test_check_types.py +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/tests/test_choices.py +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/tests/test_command_help_suggestion.py +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/tests/test_config.py +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/tests/test_config_file_path.py +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/tests/test_config_set_bugs.py +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/tests/test_deep_nesting.py +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/tests/test_dependencies.py +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/tests/test_deprecated.py +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/tests/test_dump_schema.py +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/tests/test_env.py +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/tests/test_exit_codes.py +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/tests/test_float_type.py +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/tests/test_global_flags.py +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/tests/test_help.py +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/tests/test_int_type.py +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/tests/test_mutex.py +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/tests/test_nesting.py +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/tests/test_parser.py +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/tests/test_repeatable.py +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/tests/test_tagdsl.py +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/tests/test_toml_loading.py +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/tests/test_unique.py +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/tests/test_utilities.py +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/tests/test_validate.py +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/tests/test_variadic.py +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/todo/.defer/deferred.md +0 -0
- {strictcli-0.17.0 → strictcli-0.19.0}/todo/.done/original-idea.md +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
436f8a9ce1b048614cb66e929969dfbcb0791689
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
## 0.17.0
|
|
2
|
+
|
|
3
|
+
### Breaking
|
|
4
|
+
|
|
5
|
+
- **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.
|
|
6
|
+
|
|
7
|
+
### Features
|
|
8
|
+
|
|
9
|
+
- **New.** Help text now displays non-empty repeatable flag defaults as `[default: a, b, c]`.
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
{"commits":["ab63f2e29d95b834aef6e3307d23b53e3f845afe","fc7ae2927aa12b7627c3119c818d9a7e89209e39","db1924cbd143285c857324a8fcf2110718080bfa"],"user_facing":true,"description":"**New.** Public API for running checks programmatically: `App.run_checks()`, `format_check_results()`, `format_check_results_json()`, `CheckRunResult`.","type":"feature"}
|
|
2
|
+
{"commits":["6407ea33c3a16a83dcd7c362cda8e98980074a73","edfcf45b0eb1ba4896fcff1d31ea9993c9b363a1","fcaae9bb7895805308a6b9299158ada7f6d45f3b","b6720d2dd1063bff0b49f6d5624608672c0cac33"],"user_facing":false}
|
|
3
|
+
{"commits":["9cc04830fa5b7eef0d87b249e57248415fd769a5","4e89101a5fdb8c0a6f46b62829f22e09b79ec2d1"],"user_facing":false}
|
|
4
|
+
{"commits":["67ee39ff3cdf4d09a9a29c0bdcb9853999878ee9"],"user_facing":false}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
{"commits":["03d0cd60c4d207b568a19bf6916f58aacdac1450","5a1368de51cbe0abe465f21f3a38f78ba1746fef"],"user_facing":true,"description":"**Breaking.** Renamed `Tag` to `FlagSet` and `tags=` parameter to `flag_sets=` across all API surfaces.","type":"breaking"}
|
|
2
|
+
{"commits":["03782821de6b48073259aa3edb66b141ab70eb3b","232a6e47db207159abfdb81a5e1b93c1cfce82ea","fa29d925446a00a2bddec93cb949f91f13682704"],"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 = "Public API for running checks programmatically"
|
|
5
|
+
# Optional context explaining why these changes were made
|
|
6
|
+
context = "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."
|
|
7
|
+
include = ["pypi", "npm"]
|
|
8
|
+
exclude = []
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.69.2
|
|
@@ -2,9 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
# Changelog
|
|
4
4
|
|
|
5
|
-
## 0.
|
|
5
|
+
## 0.19.0
|
|
6
|
+
|
|
7
|
+
Renamed Tag to FlagSet
|
|
8
|
+
|
|
9
|
+
<details>
|
|
10
|
+
<summary>Context</summary>
|
|
11
|
+
|
|
12
|
+
Breaking: Tag class renamed to FlagSet, tags= parameter renamed to flag_sets= across all API surfaces. The Tag name was misleading — it implied labeling/annotation, but the type is actually a reusable bundle of flags.
|
|
13
|
+
|
|
14
|
+
</details>
|
|
15
|
+
|
|
16
|
+
### Breaking
|
|
6
17
|
|
|
7
|
-
|
|
18
|
+
- **Breaking.** Renamed `Tag` to `FlagSet` and `tags=` parameter to `flag_sets=` across all API surfaces.
|
|
19
|
+
|
|
20
|
+
## 0.18.0
|
|
21
|
+
|
|
22
|
+
### Features
|
|
23
|
+
|
|
24
|
+
- **New.** Public API for running checks programmatically: `App.run_checks()`, `format_check_results()`, `format_check_results_json()`, `CheckRunResult`.
|
|
25
|
+
|
|
26
|
+
## 0.17.0
|
|
8
27
|
|
|
9
28
|
### Breaking
|
|
10
29
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: strictcli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.19.0
|
|
4
4
|
Summary: A strict, zero-dependency CLI framework for Python
|
|
5
5
|
Project-URL: Homepage, https://github.com/smm-h/strictcli
|
|
6
6
|
Project-URL: Repository, https://github.com/smm-h/strictcli
|
|
@@ -222,12 +222,12 @@ Values resolve in this order: CLI argument > environment variable > default. If
|
|
|
222
222
|
|
|
223
223
|
Bool flags from env vars accept `1`, `true`, `yes` (case-insensitive) for `True` and `0`, `false`, `no` for `False`. Any other value is an error.
|
|
224
224
|
|
|
225
|
-
##
|
|
225
|
+
## FlagSets
|
|
226
226
|
|
|
227
|
-
|
|
227
|
+
FlagSets are reusable bundles of flags that can be applied to multiple commands:
|
|
228
228
|
|
|
229
229
|
```python
|
|
230
|
-
|
|
230
|
+
auth_flags = strictcli.FlagSet(
|
|
231
231
|
name="auth",
|
|
232
232
|
flags=[
|
|
233
233
|
strictcli.Flag(name="token", type=str, help="auth token", env="MYAPP_TOKEN", default=""),
|
|
@@ -235,16 +235,16 @@ auth_tag = strictcli.Tag(
|
|
|
235
235
|
],
|
|
236
236
|
)
|
|
237
237
|
|
|
238
|
-
@app.command("deploy", help="deploy the app",
|
|
238
|
+
@app.command("deploy", help="deploy the app", flag_sets=[auth_flags])
|
|
239
239
|
def deploy(token, insecure):
|
|
240
240
|
print(f"token={'set' if token else 'unset'}, insecure={insecure}")
|
|
241
241
|
|
|
242
|
-
@app.command("status", help="check status",
|
|
242
|
+
@app.command("status", help="check status", flag_sets=[auth_flags])
|
|
243
243
|
def status(token, insecure):
|
|
244
244
|
print(f"checking status")
|
|
245
245
|
```
|
|
246
246
|
|
|
247
|
-
Both commands now have `--token` and `--insecure` flags.
|
|
247
|
+
Both commands now have `--token` and `--insecure` flags. FlagSet flags appear in help output and are parsed like any other flag.
|
|
248
248
|
|
|
249
249
|
## Mutex Groups
|
|
250
250
|
|
|
@@ -204,12 +204,12 @@ Values resolve in this order: CLI argument > environment variable > default. If
|
|
|
204
204
|
|
|
205
205
|
Bool flags from env vars accept `1`, `true`, `yes` (case-insensitive) for `True` and `0`, `false`, `no` for `False`. Any other value is an error.
|
|
206
206
|
|
|
207
|
-
##
|
|
207
|
+
## FlagSets
|
|
208
208
|
|
|
209
|
-
|
|
209
|
+
FlagSets are reusable bundles of flags that can be applied to multiple commands:
|
|
210
210
|
|
|
211
211
|
```python
|
|
212
|
-
|
|
212
|
+
auth_flags = strictcli.FlagSet(
|
|
213
213
|
name="auth",
|
|
214
214
|
flags=[
|
|
215
215
|
strictcli.Flag(name="token", type=str, help="auth token", env="MYAPP_TOKEN", default=""),
|
|
@@ -217,16 +217,16 @@ auth_tag = strictcli.Tag(
|
|
|
217
217
|
],
|
|
218
218
|
)
|
|
219
219
|
|
|
220
|
-
@app.command("deploy", help="deploy the app",
|
|
220
|
+
@app.command("deploy", help="deploy the app", flag_sets=[auth_flags])
|
|
221
221
|
def deploy(token, insecure):
|
|
222
222
|
print(f"token={'set' if token else 'unset'}, insecure={insecure}")
|
|
223
223
|
|
|
224
|
-
@app.command("status", help="check status",
|
|
224
|
+
@app.command("status", help="check status", flag_sets=[auth_flags])
|
|
225
225
|
def status(token, insecure):
|
|
226
226
|
print(f"checking status")
|
|
227
227
|
```
|
|
228
228
|
|
|
229
|
-
Both commands now have `--token` and `--insecure` flags.
|
|
229
|
+
Both commands now have `--token` and `--insecure` flags. FlagSet flags appear in help output and are parsed like any other flag.
|
|
230
230
|
|
|
231
231
|
## Mutex Groups
|
|
232
232
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "strictcli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "strictcli",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.19.0",
|
|
10
10
|
"hasInstallScript": true,
|
|
11
11
|
"license": "MIT"
|
|
12
12
|
}
|
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
__version__ = "0.
|
|
5
|
+
__version__ = "0.19.0"
|
|
6
6
|
|
|
7
7
|
__all__ = [
|
|
8
|
-
"App", "Flag", "Arg", "
|
|
8
|
+
"App", "Flag", "Arg", "FlagSet", "MutexGroup", "CoRequired", "Requires",
|
|
9
9
|
"Implies", "Passthrough", "DeprecatedCommand", "Result", "flag", "arg",
|
|
10
|
-
"CheckResult", "CheckContext",
|
|
10
|
+
"CheckResult", "CheckContext", "CheckRunResult",
|
|
11
|
+
"format_check_results", "format_check_results_json",
|
|
11
12
|
]
|
|
12
13
|
|
|
13
14
|
import contextlib
|
|
@@ -676,7 +677,7 @@ class Arg:
|
|
|
676
677
|
|
|
677
678
|
|
|
678
679
|
@dataclass
|
|
679
|
-
class
|
|
680
|
+
class FlagSet:
|
|
680
681
|
"""A reusable bundle of flags."""
|
|
681
682
|
|
|
682
683
|
name: str
|
|
@@ -739,7 +740,7 @@ class Command:
|
|
|
739
740
|
handler: Callable | None
|
|
740
741
|
flags: list[Flag] = field(default_factory=list)
|
|
741
742
|
args: list[Arg] = field(default_factory=list)
|
|
742
|
-
|
|
743
|
+
flag_sets: list[FlagSet] = field(default_factory=list)
|
|
743
744
|
mutex: list[MutexGroup] = field(default_factory=list)
|
|
744
745
|
dependencies: list[CoRequired | Requires | Implies] = field(default_factory=list)
|
|
745
746
|
passthrough: Passthrough | None = None
|
|
@@ -804,7 +805,7 @@ class Group:
|
|
|
804
805
|
*,
|
|
805
806
|
help: str,
|
|
806
807
|
args: list[Arg] | None = None,
|
|
807
|
-
|
|
808
|
+
flag_sets: list[FlagSet] | None = None,
|
|
808
809
|
mutex: list[MutexGroup] | None = None,
|
|
809
810
|
dependencies: list[CoRequired | Requires | Implies] | None = None,
|
|
810
811
|
passthrough: Passthrough | None = None,
|
|
@@ -817,7 +818,7 @@ class Group:
|
|
|
817
818
|
f'command "{name}" collides with an existing group'
|
|
818
819
|
)
|
|
819
820
|
cmd = _build_and_validate_command(
|
|
820
|
-
name, help=help, handler=func, args=args,
|
|
821
|
+
name, help=help, handler=func, args=args, flag_sets=flag_sets, mutex=mutex,
|
|
821
822
|
dependencies=dependencies,
|
|
822
823
|
env_prefix=self.env_prefix,
|
|
823
824
|
global_flags=self._global_flags,
|
|
@@ -856,6 +857,14 @@ class CheckResult:
|
|
|
856
857
|
raise ValueError("CheckResult.message must be a non-empty string")
|
|
857
858
|
|
|
858
859
|
|
|
860
|
+
@dataclass(frozen=True)
|
|
861
|
+
class CheckRunResult:
|
|
862
|
+
"""A named check result returned by App.run_checks()."""
|
|
863
|
+
|
|
864
|
+
name: str
|
|
865
|
+
result: CheckResult
|
|
866
|
+
|
|
867
|
+
|
|
859
868
|
@runtime_checkable
|
|
860
869
|
class CheckContext(Protocol):
|
|
861
870
|
"""Minimal interface that tool-specific check contexts must satisfy."""
|
|
@@ -1001,6 +1010,38 @@ class App:
|
|
|
1001
1010
|
"""
|
|
1002
1011
|
self._check_context_factory = factory
|
|
1003
1012
|
|
|
1013
|
+
def run_checks(
|
|
1014
|
+
self,
|
|
1015
|
+
context: CheckContext,
|
|
1016
|
+
*,
|
|
1017
|
+
tag_expr: str | None = None,
|
|
1018
|
+
name_glob: str | None = None,
|
|
1019
|
+
run_all: bool = False,
|
|
1020
|
+
ignore_warnings: bool = False,
|
|
1021
|
+
) -> tuple[list[CheckRunResult], int]:
|
|
1022
|
+
"""Run checks programmatically with filtering and dependency resolution.
|
|
1023
|
+
|
|
1024
|
+
Returns (results, exit_code) where results is a list of CheckRunResult
|
|
1025
|
+
and exit_code is 0 if all pass (or all warn with ignore_warnings), else 1.
|
|
1026
|
+
"""
|
|
1027
|
+
if not self._checks_enabled:
|
|
1028
|
+
raise ValueError("checks are not enabled on this App")
|
|
1029
|
+
err = self._validate_check_registrations()
|
|
1030
|
+
if err:
|
|
1031
|
+
raise ValueError(err)
|
|
1032
|
+
selected = _filter_checks(self._check_defs, tag_expr, name_glob, run_all)
|
|
1033
|
+
if not selected:
|
|
1034
|
+
return ([], 0)
|
|
1035
|
+
order = _resolve_check_order(self._check_defs, selected)
|
|
1036
|
+
raw_results, exit_code = _run_checks(
|
|
1037
|
+
self._check_defs, order, context, ignore_warnings,
|
|
1038
|
+
)
|
|
1039
|
+
results = [
|
|
1040
|
+
CheckRunResult(name=name, result=result)
|
|
1041
|
+
for name, result in raw_results
|
|
1042
|
+
]
|
|
1043
|
+
return (results, exit_code)
|
|
1044
|
+
|
|
1004
1045
|
def _register_check_command(self) -> None:
|
|
1005
1046
|
"""Register the auto-generated 'check' command when checks.toml exists."""
|
|
1006
1047
|
app_ref = self # capture for closure
|
|
@@ -1048,14 +1089,19 @@ class App:
|
|
|
1048
1089
|
)
|
|
1049
1090
|
return 1
|
|
1050
1091
|
context = app_ref._check_context_factory()
|
|
1051
|
-
|
|
1052
|
-
app_ref, order, context, ignore_warnings,
|
|
1092
|
+
raw_results, exit_code = _run_checks(
|
|
1093
|
+
app_ref._check_defs, order, context, ignore_warnings,
|
|
1053
1094
|
)
|
|
1054
1095
|
|
|
1096
|
+
results_wrapped = [
|
|
1097
|
+
CheckRunResult(name=n, result=r) for n, r in raw_results
|
|
1098
|
+
]
|
|
1055
1099
|
if json:
|
|
1056
|
-
|
|
1100
|
+
print(format_check_results_json(results_wrapped))
|
|
1057
1101
|
else:
|
|
1058
|
-
|
|
1102
|
+
output = format_check_results(results_wrapped, verbose)
|
|
1103
|
+
if output:
|
|
1104
|
+
print(output)
|
|
1059
1105
|
|
|
1060
1106
|
return exit_code
|
|
1061
1107
|
|
|
@@ -1078,7 +1124,7 @@ class App:
|
|
|
1078
1124
|
help="Run project checks",
|
|
1079
1125
|
handler=_check_handler,
|
|
1080
1126
|
args=None,
|
|
1081
|
-
|
|
1127
|
+
flag_sets=None,
|
|
1082
1128
|
mutex=None,
|
|
1083
1129
|
dependencies=None,
|
|
1084
1130
|
env_prefix=self.env_prefix,
|
|
@@ -1094,7 +1140,7 @@ class App:
|
|
|
1094
1140
|
*,
|
|
1095
1141
|
help: str,
|
|
1096
1142
|
args: list[Arg] | None = None,
|
|
1097
|
-
|
|
1143
|
+
flag_sets: list[FlagSet] | None = None,
|
|
1098
1144
|
mutex: list[MutexGroup] | None = None,
|
|
1099
1145
|
dependencies: list[CoRequired | Requires | Implies] | None = None,
|
|
1100
1146
|
passthrough: Passthrough | None = None,
|
|
@@ -1107,7 +1153,7 @@ class App:
|
|
|
1107
1153
|
help=help,
|
|
1108
1154
|
handler=func,
|
|
1109
1155
|
args=args,
|
|
1110
|
-
|
|
1156
|
+
flag_sets=flag_sets,
|
|
1111
1157
|
mutex=mutex,
|
|
1112
1158
|
dependencies=dependencies,
|
|
1113
1159
|
env_prefix=self.env_prefix,
|
|
@@ -2437,7 +2483,7 @@ def _build_and_validate_command(
|
|
|
2437
2483
|
help: str,
|
|
2438
2484
|
handler: Callable,
|
|
2439
2485
|
args: list[Arg] | None,
|
|
2440
|
-
|
|
2486
|
+
flag_sets: list[FlagSet] | None,
|
|
2441
2487
|
mutex: list[MutexGroup] | None,
|
|
2442
2488
|
dependencies: list[CoRequired | Requires | Implies] | None = None,
|
|
2443
2489
|
env_prefix: str | None,
|
|
@@ -2449,22 +2495,22 @@ def _build_and_validate_command(
|
|
|
2449
2495
|
if not help or not help.strip():
|
|
2450
2496
|
raise ValueError(f'command "{name}": missing help text')
|
|
2451
2497
|
|
|
2452
|
-
# Passthrough commands must not have flags, args,
|
|
2498
|
+
# Passthrough commands must not have flags, args, flag sets, or mutex groups
|
|
2453
2499
|
if passthrough is not None:
|
|
2454
2500
|
decorator_flags = list(getattr(handler, "_strictcli_flags", []))
|
|
2455
2501
|
decorator_args = list(getattr(handler, "_strictcli_args", []))
|
|
2456
2502
|
has_flags = bool(decorator_flags)
|
|
2457
2503
|
has_args = bool(args) or bool(decorator_args)
|
|
2458
|
-
|
|
2504
|
+
has_flag_sets = bool(flag_sets)
|
|
2459
2505
|
has_mutex = bool(mutex)
|
|
2460
|
-
if has_flags or has_args or
|
|
2506
|
+
if has_flags or has_args or has_flag_sets or has_mutex:
|
|
2461
2507
|
parts = []
|
|
2462
2508
|
if has_flags:
|
|
2463
2509
|
parts.append("flags")
|
|
2464
2510
|
if has_args:
|
|
2465
2511
|
parts.append("args")
|
|
2466
|
-
if
|
|
2467
|
-
parts.append("
|
|
2512
|
+
if has_flag_sets:
|
|
2513
|
+
parts.append("flag sets")
|
|
2468
2514
|
if has_mutex:
|
|
2469
2515
|
parts.append("mutex groups")
|
|
2470
2516
|
raise ValueError(
|
|
@@ -2489,11 +2535,11 @@ def _build_and_validate_command(
|
|
|
2489
2535
|
all_args = list(args) if args else []
|
|
2490
2536
|
all_args.extend(decorator_args)
|
|
2491
2537
|
|
|
2492
|
-
# Merge
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
for
|
|
2496
|
-
|
|
2538
|
+
# Merge flag sets into flags
|
|
2539
|
+
resolved_flag_sets = list(flag_sets) if flag_sets else []
|
|
2540
|
+
flag_set_flags: list[Flag] = []
|
|
2541
|
+
for flag_set in resolved_flag_sets:
|
|
2542
|
+
flag_set_flags.extend(flag_set.flags)
|
|
2497
2543
|
|
|
2498
2544
|
# Resolve mutex groups and merge their flags
|
|
2499
2545
|
resolved_mutex = list(mutex) if mutex else []
|
|
@@ -2517,13 +2563,13 @@ def _build_and_validate_command(
|
|
|
2517
2563
|
)
|
|
2518
2564
|
mutex_flag_names.add(f.name)
|
|
2519
2565
|
|
|
2520
|
-
# All flags: decorator flags +
|
|
2521
|
-
all_flags = decorator_flags +
|
|
2566
|
+
# All flags: decorator flags + flag set flags + mutex flags + extra flags
|
|
2567
|
+
all_flags = decorator_flags + flag_set_flags + mutex_flags
|
|
2522
2568
|
if extra_flags:
|
|
2523
2569
|
all_flags.extend(extra_flags)
|
|
2524
2570
|
|
|
2525
2571
|
# Validate: no duplicate flag names (catches mutex flags overlapping with
|
|
2526
|
-
# regular flags or
|
|
2572
|
+
# regular flags or flag set flags)
|
|
2527
2573
|
seen_flag_names: set[str] = set()
|
|
2528
2574
|
for f in all_flags:
|
|
2529
2575
|
if f.name in seen_flag_names:
|
|
@@ -2699,7 +2745,7 @@ def _build_and_validate_command(
|
|
|
2699
2745
|
handler=handler,
|
|
2700
2746
|
flags=all_flags,
|
|
2701
2747
|
args=all_args,
|
|
2702
|
-
|
|
2748
|
+
flag_sets=resolved_flag_sets,
|
|
2703
2749
|
mutex=resolved_mutex,
|
|
2704
2750
|
dependencies=resolved_dependencies,
|
|
2705
2751
|
)
|
|
@@ -3330,7 +3376,7 @@ def _find_cycle(
|
|
|
3330
3376
|
|
|
3331
3377
|
|
|
3332
3378
|
def _run_checks(
|
|
3333
|
-
|
|
3379
|
+
check_defs: dict,
|
|
3334
3380
|
check_names: list[str],
|
|
3335
3381
|
context: CheckContext,
|
|
3336
3382
|
ignore_warnings: bool,
|
|
@@ -3340,7 +3386,6 @@ def _run_checks(
|
|
|
3340
3386
|
Returns (results_list, exit_code). exit_code is 0 if all pass (or all
|
|
3341
3387
|
warn with ignore_warnings=True), 1 otherwise.
|
|
3342
3388
|
"""
|
|
3343
|
-
check_defs = app._check_defs
|
|
3344
3389
|
results: list[tuple[str, CheckResult]] = []
|
|
3345
3390
|
failed_checks: set[str] = set()
|
|
3346
3391
|
exit_code = 0
|
|
@@ -3440,39 +3485,43 @@ def _check_dry_run_mode(
|
|
|
3440
3485
|
print(f" {i}. {name}")
|
|
3441
3486
|
|
|
3442
3487
|
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3488
|
+
|
|
3489
|
+
def format_check_results(
|
|
3490
|
+
results: list[CheckRunResult], verbose: bool = False,
|
|
3491
|
+
) -> str:
|
|
3492
|
+
"""Format check results as a human-readable aligned string."""
|
|
3447
3493
|
if not results:
|
|
3448
|
-
return
|
|
3494
|
+
return ""
|
|
3449
3495
|
|
|
3450
|
-
name_width = max(len(name) for
|
|
3496
|
+
name_width = max(len(r.name) for r in results)
|
|
3497
|
+
lines: list[str] = []
|
|
3451
3498
|
|
|
3452
|
-
for
|
|
3453
|
-
label = _CHECK_STATUS_LABELS[result.status]
|
|
3454
|
-
|
|
3499
|
+
for r in results:
|
|
3500
|
+
label = _CHECK_STATUS_LABELS[r.result.status]
|
|
3501
|
+
lines.append(f"{label} {r.name:<{name_width}} {r.result.message}")
|
|
3455
3502
|
|
|
3456
|
-
show_details = result.details and (
|
|
3457
|
-
verbose or result.status in ("fail", "warn", "skip")
|
|
3503
|
+
show_details = r.result.details and (
|
|
3504
|
+
verbose or r.result.status in ("fail", "warn", "skip")
|
|
3458
3505
|
)
|
|
3459
3506
|
if show_details:
|
|
3460
|
-
for detail in result.details:
|
|
3461
|
-
|
|
3507
|
+
for detail in r.result.details:
|
|
3508
|
+
lines.append(f" {detail}")
|
|
3509
|
+
|
|
3510
|
+
return "\n".join(lines)
|
|
3462
3511
|
|
|
3463
3512
|
|
|
3464
|
-
def
|
|
3465
|
-
"""
|
|
3513
|
+
def format_check_results_json(results: list[CheckRunResult]) -> str:
|
|
3514
|
+
"""Format check results as a JSON string."""
|
|
3466
3515
|
items = [
|
|
3467
3516
|
{
|
|
3468
|
-
"name": name,
|
|
3469
|
-
"status": result.status,
|
|
3470
|
-
"message": result.message,
|
|
3471
|
-
"details": result.details,
|
|
3517
|
+
"name": r.name,
|
|
3518
|
+
"status": r.result.status,
|
|
3519
|
+
"message": r.result.message,
|
|
3520
|
+
"details": r.result.details if r.result.details is not None else [],
|
|
3472
3521
|
}
|
|
3473
|
-
for
|
|
3522
|
+
for r in results
|
|
3474
3523
|
]
|
|
3475
|
-
|
|
3524
|
+
return json.dumps(items, separators=(",", ":"))
|
|
3476
3525
|
|
|
3477
3526
|
|
|
3478
3527
|
# ---------------------------------------------------------------------------
|