strictcli 0.16.0__tar.gz → 0.17.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.17.0/.rlsbl/changes/.validated +1 -0
- strictcli-0.17.0/.rlsbl/changes/0.16.0.md +18 -0
- strictcli-0.17.0/.rlsbl/changes/0.16.1.jsonl +3 -0
- strictcli-0.17.0/.rlsbl/changes/0.16.1.md +6 -0
- strictcli-0.17.0/.rlsbl/changes/0.17.0.jsonl +3 -0
- strictcli-0.17.0/.rlsbl/releases/v0.16.1.toml +8 -0
- strictcli-0.17.0/.rlsbl/releases/v0.17.0.toml +8 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/CHANGELOG.md +16 -6
- {strictcli-0.16.0 → strictcli-0.17.0}/PKG-INFO +1 -1
- {strictcli-0.16.0 → strictcli-0.17.0}/package-lock.json +2 -2
- {strictcli-0.16.0 → strictcli-0.17.0}/package.json +1 -1
- {strictcli-0.16.0 → strictcli-0.17.0}/pyproject.toml +1 -1
- {strictcli-0.16.0 → strictcli-0.17.0}/strictcli/__init__.py +39 -9
- {strictcli-0.16.0 → strictcli-0.17.0}/tests/test_config.py +17 -6
- {strictcli-0.16.0 → strictcli-0.17.0}/tests/test_repeatable.py +101 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/uv.lock +1 -1
- strictcli-0.16.0/.rlsbl/changes/.validated +0 -1
- {strictcli-0.16.0 → strictcli-0.17.0}/.claude/settings.json +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.github/workflows/ci.yml +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.github/workflows/publish.yml +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.gitignore +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.npmignore +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/bases/.claude/settings.json +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/bases/.github/workflows/ci.yml +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/bases/.github/workflows/publish.yml +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/bases/.gitignore +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/bases/.npmignore +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/bases/.rlsbl/hooks/post-release.sh +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/bases/.rlsbl/hooks/pre-checks.sh +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/bases/.rlsbl/hooks/pre-release.sh +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/bases/.rlsbl/lint/go.toml +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/bases/.rlsbl/lint/npm.toml +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/bases/.rlsbl/lint/python.toml +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/bases/CHANGELOG.md +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/bases/CLAUDE.md +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/bases/LICENSE +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.10.0.jsonl +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.10.0.md +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.11.0.jsonl +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.11.0.md +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.12.0.jsonl +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.12.0.md +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.13.0.jsonl +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.13.0.md +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.14.0.jsonl +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.14.0.md +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.15.0.jsonl +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.15.0.md +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.15.1.jsonl +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.15.1.md +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.16.0.jsonl +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.4.0.jsonl +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.4.0.md +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.4.1.jsonl +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.4.1.md +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.5.0.jsonl +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.5.0.md +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.6.0.jsonl +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.6.0.md +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.6.1.jsonl +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.6.1.md +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.7.0.jsonl +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.7.0.md +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.7.1.jsonl +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.7.1.md +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.8.0.jsonl +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.8.0.md +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.8.1.jsonl +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.8.1.md +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.8.2.jsonl +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.8.2.md +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.8.3.jsonl +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.8.3.md +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.8.4.jsonl +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.8.4.md +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.8.5.jsonl +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.8.5.md +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.8.6.jsonl +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.8.6.md +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.8.7.jsonl +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.8.7.md +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.9.0.jsonl +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.9.0.md +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.9.1.jsonl +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.9.1.md +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/unreleased.jsonl +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/config.json +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/hashes.json +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/hooks/post-release.sh +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/hooks/pre-checks.sh +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/hooks/pre-release.sh +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/lint/go.toml +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/lint/npm.toml +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/lint/python.toml +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/releases/unreleased.toml +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/releases/v0.10.0.toml +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/releases/v0.11.0.toml +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/releases/v0.12.0.toml +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/releases/v0.13.0.toml +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/releases/v0.15.0.toml +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/releases/v0.15.1.toml +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/releases/v0.16.0.toml +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/releases/v0.8.5.toml +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/releases/v0.8.6.toml +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/releases/v0.8.7.toml +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/releases/v0.9.0.toml +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/releases/v0.9.1.toml +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/version +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/.strictcli/schema.json +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/CLAUDE.md +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/LICENSE +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/README.md +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/index.js +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/postinstall.js +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/tests/test_arg_default.py +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/tests/test_at_prefix.py +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/tests/test_auto_version.py +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/tests/test_check_command.py +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/tests/test_check_discovery.py +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/tests/test_check_runner.py +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/tests/test_check_schema.py +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/tests/test_check_types.py +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/tests/test_choices.py +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/tests/test_command_help_suggestion.py +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/tests/test_config_file_path.py +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/tests/test_config_set_bugs.py +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/tests/test_deep_nesting.py +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/tests/test_dependencies.py +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/tests/test_deprecated.py +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/tests/test_dump_schema.py +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/tests/test_e2e.py +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/tests/test_env.py +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/tests/test_exit_codes.py +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/tests/test_float_type.py +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/tests/test_global_flags.py +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/tests/test_help.py +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/tests/test_int_type.py +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/tests/test_mutex.py +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/tests/test_nesting.py +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/tests/test_parser.py +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/tests/test_passthrough.py +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/tests/test_registration.py +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/tests/test_tagdsl.py +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/tests/test_tags.py +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/tests/test_toml_loading.py +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/tests/test_unique.py +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/tests/test_utilities.py +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/tests/test_validate.py +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/tests/test_variadic.py +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/todo/.defer/deferred.md +0 -0
- {strictcli-0.16.0 → strictcli-0.17.0}/todo/.done/original-idea.md +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
912fca00b39e373782d5c0b88d65d2067f89a1b1
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
## 0.16.0
|
|
2
|
+
|
|
3
|
+
### Breaking
|
|
4
|
+
|
|
5
|
+
- **Config show.** `config show` now requires explicit `--plain` or `--json` flag instead of defaulting to plain text.
|
|
6
|
+
|
|
7
|
+
### Features
|
|
8
|
+
|
|
9
|
+
- **New flag field.** `unique` field on repeatable flags enforces no duplicate values at CLI parse time and in config arrays.
|
|
10
|
+
- **New flag field.** `env_separator` on repeatable flags controls how environment variable values are split into arrays.
|
|
11
|
+
- **Config arrays.** Config values for repeatable flags are now coerced from strings to the declared type.
|
|
12
|
+
- **Config set.** Repeatable flags can now be managed with `config set`, including `--clear` to reset and `--default` to restore default values.
|
|
13
|
+
|
|
14
|
+
### Fixes
|
|
15
|
+
|
|
16
|
+
- **Fix.** Float NaN/Inf env var error messages now include the environment variable suffix.
|
|
17
|
+
- **Fix.** Config error messages now use consistent type names and array formatting.
|
|
18
|
+
- **Fix.** Flag collection no longer includes config group pseudo-flags, preventing incorrect behavior in commands that enumerate all flags.
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
{"commits":["cdc010316ce356628e2592b493ccc368dd782b48"],"user_facing":true,"description":"**Fix.** `config set` mutex error messages now include the `config set:` prefix for consistency.","type":"fix"}
|
|
2
|
+
{"commits":["98bfea68b1b971b42a31ecb1d08ecc3b55df86f2","1d9ee273ce05153c7c9e23b8297c6fccdb342c21"],"user_facing":false}
|
|
3
|
+
{"commits":["67b8d456d3e491b3d398ee7b0b7a4f3ba116cab1"],"user_facing":true,"description":"**Fix.** Config set help text now mentions backslash escaping for values containing commas in repeatable flags.","type":"fix"}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
{"commits":["5660d554d8a5dbb47bb6872d8513ba497fb04e7f"],"user_facing":false}
|
|
2
|
+
{"commits":["ba0ca0dd03644fa63f6ca6525ce81d814100a7a1"],"user_facing":true,"description":"**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.","type":"breaking"}
|
|
3
|
+
{"commits":["47de2e49ebd7248a5b16a54860ec9cdf1e5e98e9"],"user_facing":true,"description":"**New.** Help text now displays non-empty repeatable flag defaults as `[default: a, b, c]`.","type":"feature"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Version bump type: patch, minor, or major
|
|
2
|
+
bump = "patch"
|
|
3
|
+
# Short description of this release (required)
|
|
4
|
+
description = "Error message fixes and help text improvements"
|
|
5
|
+
# Optional context explaining why these changes were made
|
|
6
|
+
context = ""
|
|
7
|
+
include = ["pypi", "npm"]
|
|
8
|
+
exclude = []
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Version bump type: patch, minor, or major
|
|
2
|
+
bump = "minor"
|
|
3
|
+
# Short description of this release (required)
|
|
4
|
+
description = "Repeatable flag default validation and help text display"
|
|
5
|
+
# Optional context explaining why these changes were made
|
|
6
|
+
context = ""
|
|
7
|
+
include = ["pypi", "npm"]
|
|
8
|
+
exclude = []
|
|
@@ -2,16 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
# Changelog
|
|
4
4
|
|
|
5
|
-
## 0.
|
|
5
|
+
## 0.17.0
|
|
6
|
+
|
|
7
|
+
Repeatable flag default validation and help text display
|
|
8
|
+
|
|
9
|
+
### Breaking
|
|
10
|
+
|
|
11
|
+
- **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
12
|
|
|
7
|
-
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
- **New.** Help text now displays non-empty repeatable flag defaults as `[default: a, b, c]`.
|
|
8
16
|
|
|
9
|
-
|
|
10
|
-
<summary>Context</summary>
|
|
17
|
+
## 0.16.1
|
|
11
18
|
|
|
12
|
-
|
|
19
|
+
### Fixes
|
|
13
20
|
|
|
14
|
-
|
|
21
|
+
- **Fix.** `config set` mutex error messages now include the `config set:` prefix for consistency.
|
|
22
|
+
- **Fix.** Config set help text now mentions backslash escaping for values containing commas in repeatable flags.
|
|
23
|
+
|
|
24
|
+
## 0.16.0
|
|
15
25
|
|
|
16
26
|
### Breaking
|
|
17
27
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "strictcli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "strictcli",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.17.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.17.0"
|
|
6
6
|
|
|
7
7
|
__all__ = [
|
|
8
8
|
"App", "Flag", "Arg", "Tag", "MutexGroup", "CoRequired", "Requires",
|
|
@@ -588,6 +588,38 @@ class Flag:
|
|
|
588
588
|
raise ValueError(
|
|
589
589
|
f'Flag "{self.name}": choice {c!r} is not of type {self.type.__name__}'
|
|
590
590
|
)
|
|
591
|
+
# Validate repeatable flag defaults
|
|
592
|
+
if self.repeatable and not isinstance(self.default, _MissingSentinel):
|
|
593
|
+
if self.default is not None:
|
|
594
|
+
if not isinstance(self.default, list):
|
|
595
|
+
raise ValueError(
|
|
596
|
+
f'Flag "{self.name}": repeatable flag default must be a list'
|
|
597
|
+
)
|
|
598
|
+
if len(self.default) == 0:
|
|
599
|
+
raise ValueError(
|
|
600
|
+
f'Flag "{self.name}": explicit empty default is redundant '
|
|
601
|
+
f"for repeatable flags, omit the default"
|
|
602
|
+
)
|
|
603
|
+
# Validate element types
|
|
604
|
+
type_name = {str: "str", int: "int", float: "float"}[self.type]
|
|
605
|
+
for i, elem in enumerate(self.default):
|
|
606
|
+
if self.type is str:
|
|
607
|
+
if not isinstance(elem, str):
|
|
608
|
+
raise ValueError(
|
|
609
|
+
f'Flag "{self.name}": default element {i} is not of type {type_name}'
|
|
610
|
+
)
|
|
611
|
+
elif self.type is int:
|
|
612
|
+
if not isinstance(elem, int) or isinstance(elem, bool):
|
|
613
|
+
raise ValueError(
|
|
614
|
+
f'Flag "{self.name}": default element {i} is not of type {type_name}'
|
|
615
|
+
)
|
|
616
|
+
elif self.type is float:
|
|
617
|
+
if not isinstance(elem, (int, float)) or isinstance(elem, bool):
|
|
618
|
+
raise ValueError(
|
|
619
|
+
f'Flag "{self.name}": default element {i} is not of type {type_name}'
|
|
620
|
+
)
|
|
621
|
+
if isinstance(elem, int):
|
|
622
|
+
self.default[i] = float(elem)
|
|
591
623
|
# Validate default type for int flags
|
|
592
624
|
if self.type is int and not isinstance(self.default, _MissingSentinel) and self.default is not None:
|
|
593
625
|
if not self.repeatable and not isinstance(self.default, int):
|
|
@@ -1163,11 +1195,7 @@ class App:
|
|
|
1163
1195
|
|
|
1164
1196
|
# config show
|
|
1165
1197
|
def _config_show_handler(**_kw) -> int:
|
|
1166
|
-
use_plain = _kw.get("plain", False)
|
|
1167
1198
|
use_json = _kw.get("json", False)
|
|
1168
|
-
if not use_plain and not use_json:
|
|
1169
|
-
print("config show: specify --plain or --json", file=sys.stderr)
|
|
1170
|
-
return 1
|
|
1171
1199
|
config_data = _load_config(
|
|
1172
1200
|
app_ref.name,
|
|
1173
1201
|
config_path_override=app_ref.config_path,
|
|
@@ -1251,7 +1279,7 @@ class App:
|
|
|
1251
1279
|
# Validate: exactly one of (value, --clear, --default)
|
|
1252
1280
|
has_value = value is not None
|
|
1253
1281
|
if use_clear and use_default:
|
|
1254
|
-
print("--clear and --default are mutually exclusive",
|
|
1282
|
+
print("config set: --clear and --default are mutually exclusive",
|
|
1255
1283
|
file=sys.stderr)
|
|
1256
1284
|
return 1
|
|
1257
1285
|
if has_value and use_clear:
|
|
@@ -1369,7 +1397,7 @@ class App:
|
|
|
1369
1397
|
args=[
|
|
1370
1398
|
Arg(name="key", help="Config key to set"),
|
|
1371
1399
|
Arg(name="value",
|
|
1372
|
-
help="Value to set (comma-separated for repeatable flags)",
|
|
1400
|
+
help="Value to set (comma-separated for repeatable flags, use backslash to escape commas)",
|
|
1373
1401
|
required=False),
|
|
1374
1402
|
],
|
|
1375
1403
|
flags=[
|
|
@@ -2908,8 +2936,10 @@ def _build_flag_meta(f: Flag) -> str:
|
|
|
2908
2936
|
if f.type is bool:
|
|
2909
2937
|
meta_parts.append(f"default: {'true' if f.default else 'false'}")
|
|
2910
2938
|
elif f.repeatable:
|
|
2911
|
-
# Repeatable flags are never required;
|
|
2912
|
-
|
|
2939
|
+
# Repeatable flags are never required; show default only if non-empty
|
|
2940
|
+
if f.default:
|
|
2941
|
+
joined = ", ".join(_format_value_for_error(elem) for elem in f.default)
|
|
2942
|
+
meta_parts.append(f"default: {joined}")
|
|
2913
2943
|
elif f.default is not None:
|
|
2914
2944
|
meta_parts.append(f"default: {f.default}")
|
|
2915
2945
|
else:
|
|
@@ -1114,14 +1114,13 @@ def _make_config_set_app(config_path=None, config_format="json"):
|
|
|
1114
1114
|
)
|
|
1115
1115
|
|
|
1116
1116
|
@app.command("run", help="run something")
|
|
1117
|
-
@strictcli.flag("tags", type=str, help="tags", repeatable=True, unique=False
|
|
1118
|
-
default=[])
|
|
1117
|
+
@strictcli.flag("tags", type=str, help="tags", repeatable=True, unique=False)
|
|
1119
1118
|
@strictcli.flag("counts", type=int, help="counts", repeatable=True,
|
|
1120
|
-
unique=False
|
|
1119
|
+
unique=False)
|
|
1121
1120
|
@strictcli.flag("rates", type=float, help="rates", repeatable=True,
|
|
1122
|
-
unique=False
|
|
1121
|
+
unique=False)
|
|
1123
1122
|
@strictcli.flag("ids", type=int, help="unique ids", repeatable=True,
|
|
1124
|
-
unique=True
|
|
1123
|
+
unique=True)
|
|
1125
1124
|
@strictcli.flag("name", type=str, help="name", default="default")
|
|
1126
1125
|
def run(tags, counts, rates, ids, name):
|
|
1127
1126
|
print(f"tags={tags} counts={counts} rates={rates} ids={ids} name={name}")
|
|
@@ -1153,6 +1152,18 @@ def test_config_set_repeatable_int(tmp_path, monkeypatch):
|
|
|
1153
1152
|
assert data["counts"] == [1, 2, 3]
|
|
1154
1153
|
|
|
1155
1154
|
|
|
1155
|
+
def test_config_set_repeatable_float(tmp_path, monkeypatch):
|
|
1156
|
+
"""Config set writes a float array for a repeatable flag."""
|
|
1157
|
+
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path))
|
|
1158
|
+
app = _make_config_set_app()
|
|
1159
|
+
r = app.test(["config", "set", "rates", "1.5,2.5,3.0"])
|
|
1160
|
+
assert r.exit_code == 0
|
|
1161
|
+
|
|
1162
|
+
config_file = tmp_path / "repapp" / "config.json"
|
|
1163
|
+
data = json.loads(config_file.read_text())
|
|
1164
|
+
assert data["rates"] == [1.5, 2.5, 3.0]
|
|
1165
|
+
|
|
1166
|
+
|
|
1156
1167
|
def test_config_set_escaped_comma(tmp_path, monkeypatch):
|
|
1157
1168
|
r"""Config set handles escaped commas: a\,b,c -> ["a,b", "c"]."""
|
|
1158
1169
|
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path))
|
|
@@ -1297,4 +1308,4 @@ def test_config_set_clear_and_default_error(tmp_path, monkeypatch):
|
|
|
1297
1308
|
app = _make_config_set_app()
|
|
1298
1309
|
r = app.test(["config", "set", "tags", "--clear", "--default"])
|
|
1299
1310
|
assert r.exit_code == 1
|
|
1300
|
-
assert "--clear and --default are mutually exclusive" in r.stderr
|
|
1311
|
+
assert "config set: --clear and --default are mutually exclusive" in r.stderr
|
|
@@ -534,3 +534,104 @@ def test_env_separator_float_nan_error(monkeypatch):
|
|
|
534
534
|
r = app.test(["cmd"])
|
|
535
535
|
assert r.exit_code == 1
|
|
536
536
|
assert "--rate: NaN is not allowed (from env var 'RATES')" in r.stderr
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
# --- Repeatable flag default validation ---
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
def test_repeatable_default_must_be_list():
|
|
543
|
+
"""Repeatable flag with non-list default raises ValueError."""
|
|
544
|
+
with pytest.raises(ValueError, match='Flag "record": repeatable flag default must be a list'):
|
|
545
|
+
_make_app_with_repeatable(type=str, default="not a list")
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def test_repeatable_default_empty_list_error():
|
|
549
|
+
"""Repeatable flag with explicit default=[] raises ValueError."""
|
|
550
|
+
with pytest.raises(
|
|
551
|
+
ValueError,
|
|
552
|
+
match='Flag "record": explicit empty default is redundant for repeatable flags, omit the default',
|
|
553
|
+
):
|
|
554
|
+
_make_app_with_repeatable(type=str, default=[])
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
def test_repeatable_default_wrong_element_type_str():
|
|
558
|
+
"""Repeatable str flag with default=[1] raises ValueError at element 0."""
|
|
559
|
+
with pytest.raises(ValueError, match='Flag "record": default element 0 is not of type str'):
|
|
560
|
+
_make_app_with_repeatable(type=str, default=[1])
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
def test_repeatable_default_wrong_element_type_int():
|
|
564
|
+
"""Repeatable int flag with default=["x"] raises ValueError."""
|
|
565
|
+
with pytest.raises(ValueError, match='Flag "record": default element 0 is not of type int'):
|
|
566
|
+
_make_app_with_repeatable(type=int, default=["x"])
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
def test_repeatable_default_int_coerced_to_float():
|
|
570
|
+
"""Repeatable float flag with default=[1, 2] coerces to [1.0, 2.0]."""
|
|
571
|
+
app = strictcli.App(name="test", version="1.0.0", help="test app")
|
|
572
|
+
|
|
573
|
+
@app.command("cmd", help="a command")
|
|
574
|
+
@strictcli.flag("val", type=float, help="a value", repeatable=True, unique=False,
|
|
575
|
+
default=[1, 2])
|
|
576
|
+
def cmd(val):
|
|
577
|
+
print(f"val={val!r}")
|
|
578
|
+
|
|
579
|
+
# The flag object should have coerced defaults
|
|
580
|
+
flag_obj = app._commands["cmd"].flags[0]
|
|
581
|
+
assert flag_obj.default == [1.0, 2.0]
|
|
582
|
+
assert all(isinstance(x, float) for x in flag_obj.default)
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
def test_repeatable_default_valid():
|
|
586
|
+
"""Repeatable flag with valid non-empty default succeeds."""
|
|
587
|
+
app = _make_app_with_repeatable(type=str, default=["a", "b"])
|
|
588
|
+
r = app.test(["cmd"])
|
|
589
|
+
assert r.exit_code == 0
|
|
590
|
+
assert "record=['a', 'b']" in r.stdout
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
def test_repeatable_default_applied():
|
|
594
|
+
"""Regression guard: repeatable flag default is applied when no CLI/env value."""
|
|
595
|
+
app = strictcli.App(name="test", version="1.0.0", help="test app")
|
|
596
|
+
|
|
597
|
+
@app.command("cmd", help="a command")
|
|
598
|
+
@strictcli.flag("tag", help="a tag", repeatable=True, unique=False,
|
|
599
|
+
default=["a", "b"])
|
|
600
|
+
def cmd(tag):
|
|
601
|
+
print(f"tag={tag!r}")
|
|
602
|
+
|
|
603
|
+
r = app.test(["cmd"])
|
|
604
|
+
assert r.exit_code == 0
|
|
605
|
+
assert "tag=['a', 'b']" in r.stdout
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
def test_repeatable_default_shown_in_help():
|
|
609
|
+
"""Repeatable flag with non-empty default shows it in help text."""
|
|
610
|
+
app = _make_app_with_repeatable(type=str, default=["x", "y"])
|
|
611
|
+
r = app.test(["cmd", "--help"])
|
|
612
|
+
assert r.exit_code == 0
|
|
613
|
+
assert "[default: x, y]" in r.stdout
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
def test_repeatable_no_default_not_in_help():
|
|
617
|
+
"""Repeatable flag with no explicit default shows no default in help."""
|
|
618
|
+
app = _make_app_with_repeatable(type=str)
|
|
619
|
+
r = app.test(["cmd", "--help"])
|
|
620
|
+
assert r.exit_code == 0
|
|
621
|
+
assert "[default" not in r.stdout
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
def test_repeatable_default_int_in_help():
|
|
625
|
+
"""Repeatable int flag with default shows formatted ints in help."""
|
|
626
|
+
app = _make_app_with_repeatable(type=int, default=[1, 2, 3])
|
|
627
|
+
r = app.test(["cmd", "--help"])
|
|
628
|
+
assert r.exit_code == 0
|
|
629
|
+
assert "[default: 1, 2, 3]" in r.stdout
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
def test_repeatable_default_float_in_help():
|
|
633
|
+
"""Repeatable float flag with default shows formatted floats in help."""
|
|
634
|
+
app = _make_app_with_repeatable(type=float, default=[1.5, 2.5])
|
|
635
|
+
r = app.test(["cmd", "--help"])
|
|
636
|
+
assert r.exit_code == 0
|
|
637
|
+
assert "[default: 1.5, 2.5]" in r.stdout
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
6e78d5bbca2712fb8e63e4316ea118fff59b992f
|
|
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
|
|
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
|
|
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
|
|
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
|