strictcli 0.14.0__tar.gz → 0.15.1__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.15.1/.rlsbl/changes/.validated +1 -0
- strictcli-0.15.1/.rlsbl/changes/0.15.0.jsonl +6 -0
- strictcli-0.15.1/.rlsbl/changes/0.15.0.md +9 -0
- strictcli-0.15.1/.rlsbl/changes/0.15.1.jsonl +6 -0
- strictcli-0.15.1/.rlsbl/changes/0.15.1.md +9 -0
- strictcli-0.15.1/.rlsbl/config.json +12 -0
- strictcli-0.15.1/.rlsbl/releases/unreleased.toml +0 -0
- strictcli-0.15.1/.rlsbl/releases/v0.15.0.toml +8 -0
- strictcli-0.15.1/.rlsbl/releases/v0.15.1.toml +8 -0
- strictcli-0.15.1/.rlsbl/version +1 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/CHANGELOG.md +23 -3
- {strictcli-0.14.0 → strictcli-0.15.1}/PKG-INFO +1 -1
- {strictcli-0.14.0 → strictcli-0.15.1}/package-lock.json +2 -2
- {strictcli-0.14.0 → strictcli-0.15.1}/package.json +1 -1
- {strictcli-0.14.0 → strictcli-0.15.1}/pyproject.toml +1 -1
- {strictcli-0.14.0 → strictcli-0.15.1}/strictcli/__init__.py +124 -85
- {strictcli-0.14.0 → strictcli-0.15.1}/tests/test_config.py +28 -1
- strictcli-0.15.1/tests/test_config_set_bugs.py +266 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/uv.lock +1 -1
- strictcli-0.14.0/.rlsbl/changes/.validated +0 -1
- strictcli-0.14.0/.rlsbl/config.json +0 -8
- strictcli-0.14.0/.rlsbl/version +0 -1
- {strictcli-0.14.0 → strictcli-0.15.1}/.claude/settings.json +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.github/workflows/ci.yml +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.github/workflows/publish.yml +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.gitignore +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.npmignore +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/bases/.claude/settings.json +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/bases/.github/workflows/ci.yml +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/bases/.github/workflows/publish.yml +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/bases/.gitignore +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/bases/.npmignore +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/bases/.rlsbl/hooks/post-release.sh +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/bases/.rlsbl/hooks/pre-checks.sh +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/bases/.rlsbl/hooks/pre-release.sh +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/bases/.rlsbl/lint/go.toml +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/bases/.rlsbl/lint/npm.toml +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/bases/.rlsbl/lint/python.toml +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/bases/CHANGELOG.md +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/bases/CLAUDE.md +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/bases/LICENSE +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.10.0.jsonl +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.10.0.md +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.11.0.jsonl +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.11.0.md +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.12.0.jsonl +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.12.0.md +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.13.0.jsonl +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.13.0.md +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.14.0.jsonl +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.14.0.md +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.4.0.jsonl +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.4.0.md +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.4.1.jsonl +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.4.1.md +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.5.0.jsonl +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.5.0.md +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.6.0.jsonl +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.6.0.md +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.6.1.jsonl +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.6.1.md +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.7.0.jsonl +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.7.0.md +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.7.1.jsonl +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.7.1.md +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.8.0.jsonl +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.8.0.md +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.8.1.jsonl +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.8.1.md +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.8.2.jsonl +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.8.2.md +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.8.3.jsonl +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.8.3.md +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.8.4.jsonl +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.8.4.md +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.8.5.jsonl +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.8.5.md +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.8.6.jsonl +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.8.6.md +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.8.7.jsonl +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.8.7.md +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.9.0.jsonl +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.9.0.md +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.9.1.jsonl +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.9.1.md +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/unreleased.jsonl +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/hashes.json +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/hooks/post-release.sh +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/hooks/pre-checks.sh +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/hooks/pre-release.sh +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/lint/go.toml +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/lint/npm.toml +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/lint/python.toml +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/releases/v0.10.0.toml +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/releases/v0.11.0.toml +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/releases/v0.12.0.toml +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/releases/v0.13.0.toml +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/releases/v0.8.5.toml +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/releases/v0.8.6.toml +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/releases/v0.8.7.toml +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/releases/v0.9.0.toml +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/releases/v0.9.1.toml +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/.strictcli/schema.json +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/CLAUDE.md +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/LICENSE +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/README.md +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/index.js +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/postinstall.js +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/tests/test_arg_default.py +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/tests/test_at_prefix.py +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/tests/test_auto_version.py +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/tests/test_check_command.py +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/tests/test_check_discovery.py +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/tests/test_check_runner.py +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/tests/test_check_schema.py +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/tests/test_check_types.py +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/tests/test_choices.py +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/tests/test_command_help_suggestion.py +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/tests/test_config_file_path.py +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/tests/test_deep_nesting.py +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/tests/test_dependencies.py +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/tests/test_deprecated.py +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/tests/test_dump_schema.py +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/tests/test_e2e.py +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/tests/test_env.py +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/tests/test_exit_codes.py +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/tests/test_float_type.py +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/tests/test_global_flags.py +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/tests/test_help.py +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/tests/test_int_type.py +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/tests/test_mutex.py +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/tests/test_nesting.py +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/tests/test_parser.py +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/tests/test_passthrough.py +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/tests/test_registration.py +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/tests/test_repeatable.py +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/tests/test_tagdsl.py +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/tests/test_tags.py +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/tests/test_toml_loading.py +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/tests/test_validate.py +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/tests/test_variadic.py +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/todo/.defer/deferred.md +0 -0
- {strictcli-0.14.0 → strictcli-0.15.1}/todo/.done/original-idea.md +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
4df925097e74f9a99e5ece43f3df2cd90f7b3e17
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{"commits":["057edc5200703fe2a2d37d28f488833b35254a06"],"user_facing":false}
|
|
2
|
+
{"commits":["e6af4e228227d55585c09ec9012cca846eff2651"],"user_facing":false}
|
|
3
|
+
{"commits":["e5dc999d0d920ba0737d2c565be818af8465de1c"],"user_facing":true,"description":"**Breaking.** `config set` now validates keys against registered flags and rejects unknown keys.","type":"breaking"}
|
|
4
|
+
{"commits":["e5dc999d0d920ba0737d2c565be818af8465de1c"],"user_facing":true,"description":"**New feature.** `config set` coerces values to the flag's declared type (int, bool, float) before writing to config.","type":"feature"}
|
|
5
|
+
{"commits":["4cf2df20f76491a8d9b504307de8d78ca514e7ab"],"user_facing":false}
|
|
6
|
+
{"commits":["050a53f9af245a308f6d5895307beb7f479c9c27"],"user_facing":false}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
## 0.15.0
|
|
2
|
+
|
|
3
|
+
### Breaking
|
|
4
|
+
|
|
5
|
+
- **Breaking.** `config set` now validates keys against registered flags and rejects unknown keys.
|
|
6
|
+
|
|
7
|
+
### Features
|
|
8
|
+
|
|
9
|
+
- **New feature.** `config set` coerces values to the flag's declared type (int, bool, float) before writing to config.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{"commits":["face7b1fcf296fcd3ba1d5d7b15194c71f79ac0c"],"user_facing":true,"description":"**Breaking.** Config set now accepts negative numeric values. Unknown single-dash tokens produce 'unexpected argument' instead of 'unknown flag'.","type":"breaking"}
|
|
2
|
+
{"commits":["d4bbb758e89f174279d3d1a281ded985c47c958a"],"user_facing":true,"description":"**Fix.** Config show now outputs lowercase true/false for bool values, matching Go implementation.","type":"fix"}
|
|
3
|
+
{"commits":["b2bdcfa7388ac5904c5da98a017c4248cc2737ea"],"user_facing":false}
|
|
4
|
+
{"commits":["053285501d0c135dffc782f038ac4f35881bcd89"],"user_facing":false}
|
|
5
|
+
{"commits":["48504dbc7255e89a73c663f4745eaf7a64ec0466"],"user_facing":false}
|
|
6
|
+
{"commits":["ca62cef3775568ecda2e7bdadf04ee813608d9d6"],"user_facing":false}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
## 0.15.1
|
|
2
|
+
|
|
3
|
+
### Breaking
|
|
4
|
+
|
|
5
|
+
- **Breaking.** Config set now accepts negative numeric values. Unknown single-dash tokens produce 'unexpected argument' instead of 'unknown flag'.
|
|
6
|
+
|
|
7
|
+
### Fixes
|
|
8
|
+
|
|
9
|
+
- **Fix.** Config show now outputs lowercase true/false for bool values, matching Go implementation.
|
|
File without changes
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Version bump type: patch, minor, or major
|
|
2
|
+
bump = "minor"
|
|
3
|
+
# Short description of this release (required)
|
|
4
|
+
description = "Config set type coercion and key validation"
|
|
5
|
+
# Optional context explaining why these changes were made
|
|
6
|
+
context = "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."
|
|
7
|
+
include = ["pypi", "npm"]
|
|
8
|
+
exclude = []
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Version bump type: patch, minor, or major
|
|
2
|
+
bump = "patch"
|
|
3
|
+
# Short description of this release (required)
|
|
4
|
+
description = "Config show bool parity and negative values in config set"
|
|
5
|
+
# Optional context explaining why these changes were made
|
|
6
|
+
context = "config show now outputs lowercase true/false matching Go. config set accepts negative numeric values."
|
|
7
|
+
include = ["pypi", "npm"]
|
|
8
|
+
exclude = []
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.63.0
|
|
@@ -2,17 +2,37 @@
|
|
|
2
2
|
|
|
3
3
|
# Changelog
|
|
4
4
|
|
|
5
|
-
## 0.
|
|
5
|
+
## 0.15.1
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Config show bool parity and negative values in config set
|
|
8
8
|
|
|
9
9
|
<details>
|
|
10
10
|
<summary>Context</summary>
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
config show now outputs lowercase true/false matching Go. config set accepts negative numeric values.
|
|
13
13
|
|
|
14
14
|
</details>
|
|
15
15
|
|
|
16
|
+
### Breaking
|
|
17
|
+
|
|
18
|
+
- **Breaking.** Config set now accepts negative numeric values. Unknown single-dash tokens produce 'unexpected argument' instead of 'unknown flag'.
|
|
19
|
+
|
|
20
|
+
### Fixes
|
|
21
|
+
|
|
22
|
+
- **Fix.** Config show now outputs lowercase true/false for bool values, matching Go implementation.
|
|
23
|
+
|
|
24
|
+
## 0.15.0
|
|
25
|
+
|
|
26
|
+
### Breaking
|
|
27
|
+
|
|
28
|
+
- **Breaking.** `config set` now validates keys against registered flags and rejects unknown keys.
|
|
29
|
+
|
|
30
|
+
### Features
|
|
31
|
+
|
|
32
|
+
- **New feature.** `config set` coerces values to the flag's declared type (int, bool, float) before writing to config.
|
|
33
|
+
|
|
34
|
+
## 0.14.0
|
|
35
|
+
|
|
16
36
|
### Features
|
|
17
37
|
|
|
18
38
|
- **New feature.** `checks_embed` parameter accepts raw TOML bytes, enabling inline checks configuration without a file on disk.
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "strictcli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.1",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "strictcli",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.15.1",
|
|
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.15.1"
|
|
6
6
|
|
|
7
7
|
__all__ = [
|
|
8
8
|
"App", "Flag", "Arg", "Tag", "MutexGroup", "CoRequired", "Requires",
|
|
@@ -125,6 +125,15 @@ def _coerce_config_value(value: object, flag: "Flag") -> object:
|
|
|
125
125
|
raise ValueError(f"unsupported flag type {flag.type}")
|
|
126
126
|
|
|
127
127
|
|
|
128
|
+
def _format_config_value(value: object) -> str:
|
|
129
|
+
"""Format a config value for display, matching Go's formatConfigValue."""
|
|
130
|
+
if value is None:
|
|
131
|
+
return "<nil>"
|
|
132
|
+
if isinstance(value, bool):
|
|
133
|
+
return "true" if value else "false"
|
|
134
|
+
return str(value)
|
|
135
|
+
|
|
136
|
+
|
|
128
137
|
_CHECK_NAME_RE = re.compile(r"^[a-z][a-z0-9-]*$")
|
|
129
138
|
_CHECK_REQUIRED_FIELDS = {"tags", "severity", "fast", "pure", "needs_network", "depends_on"}
|
|
130
139
|
_CHECK_VALID_SEVERITIES = {"error", "warn"}
|
|
@@ -285,18 +294,39 @@ class _ParseError(Exception):
|
|
|
285
294
|
self.command_prefix = command_prefix
|
|
286
295
|
|
|
287
296
|
|
|
297
|
+
def _strict_bool(s: str) -> bool:
|
|
298
|
+
"""Parse a boolean string strictly.
|
|
299
|
+
|
|
300
|
+
Accepts: 1, true, yes (case-insensitive) -> True
|
|
301
|
+
Accepts: 0, false, no (case-insensitive) -> False
|
|
302
|
+
Everything else raises ValueError.
|
|
303
|
+
"""
|
|
304
|
+
lower = s.lower()
|
|
305
|
+
if lower in ("1", "true", "yes"):
|
|
306
|
+
return True
|
|
307
|
+
if lower in ("0", "false", "no"):
|
|
308
|
+
return False
|
|
309
|
+
raise ValueError(f"expected boolean, got '{s}'")
|
|
310
|
+
|
|
311
|
+
|
|
288
312
|
def _strict_int(s: str) -> int:
|
|
289
313
|
"""Parse an integer string strictly -- no leading/trailing whitespace allowed.
|
|
290
314
|
|
|
291
315
|
Python's int() silently strips whitespace; Go's strconv.Atoi does not.
|
|
292
316
|
This matches Go's stricter behavior. Additionally, the result is
|
|
293
317
|
range-checked to fit in a signed 64-bit integer, matching Go's int/int64.
|
|
318
|
+
|
|
319
|
+
All errors raise ValueError with the same message format as Go's
|
|
320
|
+
parseIntStrict: "expected integer, got '<value>'".
|
|
294
321
|
"""
|
|
295
322
|
if s != s.strip():
|
|
296
|
-
raise ValueError(f"
|
|
297
|
-
|
|
323
|
+
raise ValueError(f"expected integer, got '{s}'")
|
|
324
|
+
try:
|
|
325
|
+
n = int(s)
|
|
326
|
+
except ValueError:
|
|
327
|
+
raise ValueError(f"expected integer, got '{s}'") from None
|
|
298
328
|
if n < -(2**63) or n > 2**63 - 1:
|
|
299
|
-
raise ValueError("integer
|
|
329
|
+
raise ValueError(f"expected integer, got '{s}'")
|
|
300
330
|
return n
|
|
301
331
|
|
|
302
332
|
|
|
@@ -1005,7 +1035,7 @@ class App:
|
|
|
1005
1035
|
else:
|
|
1006
1036
|
value = None
|
|
1007
1037
|
source = "default"
|
|
1008
|
-
print(f"{param} = {value} (source: {source})")
|
|
1038
|
+
print(f"{param} = {_format_config_value(value)} (source: {source})")
|
|
1009
1039
|
|
|
1010
1040
|
config_grp.commands["show"] = Command(
|
|
1011
1041
|
name="show",
|
|
@@ -1014,7 +1044,7 @@ class App:
|
|
|
1014
1044
|
)
|
|
1015
1045
|
|
|
1016
1046
|
# config set
|
|
1017
|
-
def _config_set_handler(key, value, **_kw) ->
|
|
1047
|
+
def _config_set_handler(key, value, **_kw) -> int:
|
|
1018
1048
|
path = _config_path(
|
|
1019
1049
|
app_ref.name,
|
|
1020
1050
|
override=app_ref.config_path,
|
|
@@ -1023,26 +1053,50 @@ class App:
|
|
|
1023
1053
|
dir_path = os.path.dirname(path)
|
|
1024
1054
|
os.makedirs(dir_path, exist_ok=True)
|
|
1025
1055
|
# Read existing config
|
|
1026
|
-
existing
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1056
|
+
existing = _load_config(
|
|
1057
|
+
app_ref.name,
|
|
1058
|
+
config_path_override=app_ref.config_path,
|
|
1059
|
+
config_format=app_ref.config_format,
|
|
1060
|
+
)
|
|
1061
|
+
|
|
1062
|
+
# Look up the key against registered flags
|
|
1063
|
+
all_flags = app_ref._collect_all_flags()
|
|
1064
|
+
matched_flag = None
|
|
1065
|
+
for f in all_flags:
|
|
1066
|
+
if _flag_param_name(f.name) == key:
|
|
1067
|
+
matched_flag = f
|
|
1068
|
+
break
|
|
1069
|
+
if matched_flag is None:
|
|
1070
|
+
print(f"config set: unknown key '{key}'", file=sys.stderr)
|
|
1071
|
+
return 1
|
|
1072
|
+
|
|
1073
|
+
# Coerce the string value to the flag's type
|
|
1074
|
+
try:
|
|
1075
|
+
if matched_flag.type == bool:
|
|
1076
|
+
typed_value = _strict_bool(value)
|
|
1077
|
+
elif matched_flag.type == int:
|
|
1078
|
+
typed_value = _strict_int(value)
|
|
1079
|
+
elif matched_flag.type == float:
|
|
1035
1080
|
try:
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1081
|
+
typed_value = _strict_float(value)
|
|
1082
|
+
except ValueError as fe:
|
|
1083
|
+
msg = str(fe)
|
|
1084
|
+
if msg in ("NaN is not allowed", "Inf is not allowed"):
|
|
1085
|
+
raise
|
|
1086
|
+
raise ValueError(f"expected float, got '{value}'") from fe
|
|
1087
|
+
else: # str
|
|
1088
|
+
typed_value = value
|
|
1089
|
+
except ValueError as e:
|
|
1090
|
+
print(f"config set: key '{key}': {e}", file=sys.stderr)
|
|
1091
|
+
return 1
|
|
1092
|
+
|
|
1093
|
+
existing[key] = typed_value
|
|
1041
1094
|
if app_ref.config_format == "toml":
|
|
1042
1095
|
_write_toml_flat(existing, path)
|
|
1043
1096
|
else:
|
|
1044
1097
|
with open(path, "w") as fh:
|
|
1045
1098
|
fh.write(json.dumps(existing, indent=2) + "\n")
|
|
1099
|
+
return 0
|
|
1046
1100
|
|
|
1047
1101
|
config_grp.commands["set"] = Command(
|
|
1048
1102
|
name="set",
|
|
@@ -1253,10 +1307,8 @@ class App:
|
|
|
1253
1307
|
if f.type is int:
|
|
1254
1308
|
try:
|
|
1255
1309
|
_store_value(f, _strict_int(value_part))
|
|
1256
|
-
except ValueError:
|
|
1257
|
-
raise _ParseError(
|
|
1258
|
-
f"--{f.name}: expected integer, got {value_part!r}"
|
|
1259
|
-
)
|
|
1310
|
+
except ValueError as e:
|
|
1311
|
+
raise _ParseError(f"--{f.name}: {e}")
|
|
1260
1312
|
elif f.type is float:
|
|
1261
1313
|
try:
|
|
1262
1314
|
_store_value(f, _strict_float(value_part))
|
|
@@ -1297,10 +1349,8 @@ class App:
|
|
|
1297
1349
|
if f.type is int:
|
|
1298
1350
|
try:
|
|
1299
1351
|
_store_value(f, _strict_int(raw))
|
|
1300
|
-
except ValueError:
|
|
1301
|
-
raise _ParseError(
|
|
1302
|
-
f"--{f.name}: expected integer, got {raw!r}"
|
|
1303
|
-
)
|
|
1352
|
+
except ValueError as e:
|
|
1353
|
+
raise _ParseError(f"--{f.name}: {e}")
|
|
1304
1354
|
elif f.type is float:
|
|
1305
1355
|
try:
|
|
1306
1356
|
_store_value(f, _strict_float(raw))
|
|
@@ -1328,10 +1378,8 @@ class App:
|
|
|
1328
1378
|
if f.type is int:
|
|
1329
1379
|
try:
|
|
1330
1380
|
_store_value(f, _strict_int(raw))
|
|
1331
|
-
except ValueError:
|
|
1332
|
-
raise _ParseError(
|
|
1333
|
-
f"--{f.name}: expected integer, got {raw!r}"
|
|
1334
|
-
)
|
|
1381
|
+
except ValueError as e:
|
|
1382
|
+
raise _ParseError(f"--{f.name}: {e}")
|
|
1335
1383
|
elif f.type is float:
|
|
1336
1384
|
try:
|
|
1337
1385
|
_store_value(f, _strict_float(raw))
|
|
@@ -1362,12 +1410,9 @@ class App:
|
|
|
1362
1410
|
env_val = os.environ.get(f.env)
|
|
1363
1411
|
if env_val is not None:
|
|
1364
1412
|
if f.type is bool:
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
elif lower in ("0", "false", "no"):
|
|
1369
|
-
cli_set[f.name] = False
|
|
1370
|
-
else:
|
|
1413
|
+
try:
|
|
1414
|
+
cli_set[f.name] = _strict_bool(env_val)
|
|
1415
|
+
except ValueError:
|
|
1371
1416
|
raise _ParseError(
|
|
1372
1417
|
f"invalid boolean value {env_val!r} for env var "
|
|
1373
1418
|
f"'{f.env}' (flag '--{f.name}')"
|
|
@@ -1375,10 +1420,9 @@ class App:
|
|
|
1375
1420
|
elif f.type is int:
|
|
1376
1421
|
try:
|
|
1377
1422
|
coerced = _strict_int(env_val)
|
|
1378
|
-
except ValueError:
|
|
1423
|
+
except ValueError as e:
|
|
1379
1424
|
raise _ParseError(
|
|
1380
|
-
f"--{f.name}:
|
|
1381
|
-
f"(from env var '{f.env}')"
|
|
1425
|
+
f"--{f.name}: {e} (from env var '{f.env}')"
|
|
1382
1426
|
)
|
|
1383
1427
|
cli_set[f.name] = [coerced] if f.repeatable else coerced
|
|
1384
1428
|
elif f.type is float:
|
|
@@ -1655,8 +1699,8 @@ def _parse_command(
|
|
|
1655
1699
|
if f.type is int:
|
|
1656
1700
|
try:
|
|
1657
1701
|
_store_value(f, _strict_int(value_part))
|
|
1658
|
-
except ValueError:
|
|
1659
|
-
raise _ParseError(f"--{f.name}:
|
|
1702
|
+
except ValueError as e:
|
|
1703
|
+
raise _ParseError(f"--{f.name}: {e}")
|
|
1660
1704
|
elif f.type is float:
|
|
1661
1705
|
try:
|
|
1662
1706
|
_store_value(f, _strict_float(value_part))
|
|
@@ -1697,8 +1741,8 @@ def _parse_command(
|
|
|
1697
1741
|
if f.type is int:
|
|
1698
1742
|
try:
|
|
1699
1743
|
_store_value(f, _strict_int(raw))
|
|
1700
|
-
except ValueError:
|
|
1701
|
-
raise _ParseError(f"--{f.name}:
|
|
1744
|
+
except ValueError as e:
|
|
1745
|
+
raise _ParseError(f"--{f.name}: {e}")
|
|
1702
1746
|
elif f.type is float:
|
|
1703
1747
|
try:
|
|
1704
1748
|
_store_value(f, _strict_float(raw))
|
|
@@ -1717,40 +1761,39 @@ def _parse_command(
|
|
|
1717
1761
|
continue
|
|
1718
1762
|
|
|
1719
1763
|
# -x (short form)
|
|
1720
|
-
if tok.startswith("-") and len(tok) == 2:
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
i += 1
|
|
1726
|
-
else:
|
|
1727
|
-
# str/int/float flag: consume next token as value
|
|
1728
|
-
if i + 1 < len(tokens):
|
|
1729
|
-
raw = tokens[i + 1]
|
|
1730
|
-
if f.type is int:
|
|
1731
|
-
try:
|
|
1732
|
-
_store_value(f, _strict_int(raw))
|
|
1733
|
-
except ValueError:
|
|
1734
|
-
raise _ParseError(f"--{f.name}: expected integer, got {raw!r}")
|
|
1735
|
-
elif f.type is float:
|
|
1736
|
-
try:
|
|
1737
|
-
_store_value(f, _strict_float(raw))
|
|
1738
|
-
except ValueError as e:
|
|
1739
|
-
raise _float_parse_error(f.name, raw, e)
|
|
1740
|
-
else:
|
|
1741
|
-
resolved, stdin_consumed_by[0] = _resolve_at_prefix(
|
|
1742
|
-
f.name, raw, stdin_consumed_by[0],
|
|
1743
|
-
)
|
|
1744
|
-
_store_value(f, resolved)
|
|
1745
|
-
i += 2
|
|
1746
|
-
else:
|
|
1747
|
-
raise _ParseError(f"flag '{tok}' requires a value")
|
|
1764
|
+
if tok.startswith("-") and len(tok) == 2 and tok in short_lookup:
|
|
1765
|
+
f = short_lookup[tok]
|
|
1766
|
+
if f.type is bool:
|
|
1767
|
+
cli_set[f.name] = True
|
|
1768
|
+
i += 1
|
|
1748
1769
|
else:
|
|
1749
|
-
|
|
1770
|
+
# str/int/float flag: consume next token as value
|
|
1771
|
+
if i + 1 < len(tokens):
|
|
1772
|
+
raw = tokens[i + 1]
|
|
1773
|
+
if f.type is int:
|
|
1774
|
+
try:
|
|
1775
|
+
_store_value(f, _strict_int(raw))
|
|
1776
|
+
except ValueError as e:
|
|
1777
|
+
raise _ParseError(f"--{f.name}: {e}")
|
|
1778
|
+
elif f.type is float:
|
|
1779
|
+
try:
|
|
1780
|
+
_store_value(f, _strict_float(raw))
|
|
1781
|
+
except ValueError as e:
|
|
1782
|
+
raise _float_parse_error(f.name, raw, e)
|
|
1783
|
+
else:
|
|
1784
|
+
resolved, stdin_consumed_by[0] = _resolve_at_prefix(
|
|
1785
|
+
f.name, raw, stdin_consumed_by[0],
|
|
1786
|
+
)
|
|
1787
|
+
_store_value(f, resolved)
|
|
1788
|
+
i += 2
|
|
1789
|
+
else:
|
|
1790
|
+
raise _ParseError(f"flag '{tok}' requires a value")
|
|
1750
1791
|
continue
|
|
1751
1792
|
|
|
1752
|
-
#
|
|
1753
|
-
|
|
1793
|
+
# Token starts with "-" but doesn't match any known flag;
|
|
1794
|
+
# treat as a positional arg (e.g. negative numbers like -7, -3.14)
|
|
1795
|
+
positionals.append(tok)
|
|
1796
|
+
i += 1
|
|
1754
1797
|
|
|
1755
1798
|
# Step 4: resolve env vars for flags not set by CLI
|
|
1756
1799
|
for f in cmd.flags:
|
|
@@ -1760,12 +1803,9 @@ def _parse_command(
|
|
|
1760
1803
|
env_val = os.environ.get(f.env)
|
|
1761
1804
|
if env_val is not None:
|
|
1762
1805
|
if f.type is bool:
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
elif lower in ("0", "false", "no"):
|
|
1767
|
-
cli_set[f.name] = False
|
|
1768
|
-
else:
|
|
1806
|
+
try:
|
|
1807
|
+
cli_set[f.name] = _strict_bool(env_val)
|
|
1808
|
+
except ValueError:
|
|
1769
1809
|
raise _ParseError(
|
|
1770
1810
|
f"invalid boolean value {env_val!r} for env var "
|
|
1771
1811
|
f"'{f.env}' (flag '--{f.name}')"
|
|
@@ -1773,10 +1813,9 @@ def _parse_command(
|
|
|
1773
1813
|
elif f.type is int:
|
|
1774
1814
|
try:
|
|
1775
1815
|
coerced = _strict_int(env_val)
|
|
1776
|
-
except ValueError:
|
|
1816
|
+
except ValueError as e:
|
|
1777
1817
|
raise _ParseError(
|
|
1778
|
-
f"--{f.name}:
|
|
1779
|
-
f"(from env var '{f.env}')"
|
|
1818
|
+
f"--{f.name}: {e} (from env var '{f.env}')"
|
|
1780
1819
|
)
|
|
1781
1820
|
cli_set[f.name] = [coerced] if f.repeatable else coerced
|
|
1782
1821
|
elif f.type is float:
|
|
@@ -183,7 +183,34 @@ def test_config_show(tmp_path, monkeypatch):
|
|
|
183
183
|
assert r.exit_code == 0
|
|
184
184
|
assert "target = from-config (source: config)" in r.stdout
|
|
185
185
|
assert "count = 1 (source: default)" in r.stdout
|
|
186
|
-
assert "verbose =
|
|
186
|
+
assert "verbose = false (source: default)" in r.stdout
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
# --- Test 10b: config show formats bools as lowercase (Go parity) ---
|
|
190
|
+
|
|
191
|
+
def test_config_show_lowercase_bools(tmp_path, monkeypatch):
|
|
192
|
+
"""Config show formats bools as lowercase true/false, matching Go output."""
|
|
193
|
+
config_home = _write_config(tmp_path, "testapp", {"verbose": True})
|
|
194
|
+
monkeypatch.setenv("XDG_CONFIG_HOME", config_home)
|
|
195
|
+
app = _make_config_app(config=True)
|
|
196
|
+
r = app.test(["config", "show"])
|
|
197
|
+
assert r.exit_code == 0
|
|
198
|
+
# Bool from config must be lowercase
|
|
199
|
+
assert "verbose = true (source: config)" in r.stdout
|
|
200
|
+
# None (no default for bool) must show as <nil>
|
|
201
|
+
assert "verbose = True" not in r.stdout
|
|
202
|
+
assert "verbose = False" not in r.stdout
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def test_config_show_bool_false_default_lowercase(tmp_path, monkeypatch):
|
|
206
|
+
"""Config show formats default False bool as lowercase false."""
|
|
207
|
+
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path))
|
|
208
|
+
app = _make_config_app(config=True)
|
|
209
|
+
r = app.test(["config", "show"])
|
|
210
|
+
assert r.exit_code == 0
|
|
211
|
+
# verbose has no explicit default, defaults to False -- must be lowercase
|
|
212
|
+
assert "verbose = false (source: default)" in r.stdout
|
|
213
|
+
assert "verbose = False" not in r.stdout
|
|
187
214
|
|
|
188
215
|
|
|
189
216
|
# --- Test 11: config respects XDG_CONFIG_HOME ---
|