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.
Files changed (143) hide show
  1. strictcli-0.15.1/.rlsbl/changes/.validated +1 -0
  2. strictcli-0.15.1/.rlsbl/changes/0.15.0.jsonl +6 -0
  3. strictcli-0.15.1/.rlsbl/changes/0.15.0.md +9 -0
  4. strictcli-0.15.1/.rlsbl/changes/0.15.1.jsonl +6 -0
  5. strictcli-0.15.1/.rlsbl/changes/0.15.1.md +9 -0
  6. strictcli-0.15.1/.rlsbl/config.json +12 -0
  7. strictcli-0.15.1/.rlsbl/releases/unreleased.toml +0 -0
  8. strictcli-0.15.1/.rlsbl/releases/v0.15.0.toml +8 -0
  9. strictcli-0.15.1/.rlsbl/releases/v0.15.1.toml +8 -0
  10. strictcli-0.15.1/.rlsbl/version +1 -0
  11. {strictcli-0.14.0 → strictcli-0.15.1}/CHANGELOG.md +23 -3
  12. {strictcli-0.14.0 → strictcli-0.15.1}/PKG-INFO +1 -1
  13. {strictcli-0.14.0 → strictcli-0.15.1}/package-lock.json +2 -2
  14. {strictcli-0.14.0 → strictcli-0.15.1}/package.json +1 -1
  15. {strictcli-0.14.0 → strictcli-0.15.1}/pyproject.toml +1 -1
  16. {strictcli-0.14.0 → strictcli-0.15.1}/strictcli/__init__.py +124 -85
  17. {strictcli-0.14.0 → strictcli-0.15.1}/tests/test_config.py +28 -1
  18. strictcli-0.15.1/tests/test_config_set_bugs.py +266 -0
  19. {strictcli-0.14.0 → strictcli-0.15.1}/uv.lock +1 -1
  20. strictcli-0.14.0/.rlsbl/changes/.validated +0 -1
  21. strictcli-0.14.0/.rlsbl/config.json +0 -8
  22. strictcli-0.14.0/.rlsbl/version +0 -1
  23. {strictcli-0.14.0 → strictcli-0.15.1}/.claude/settings.json +0 -0
  24. {strictcli-0.14.0 → strictcli-0.15.1}/.github/workflows/ci.yml +0 -0
  25. {strictcli-0.14.0 → strictcli-0.15.1}/.github/workflows/publish.yml +0 -0
  26. {strictcli-0.14.0 → strictcli-0.15.1}/.gitignore +0 -0
  27. {strictcli-0.14.0 → strictcli-0.15.1}/.npmignore +0 -0
  28. {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/bases/.claude/settings.json +0 -0
  29. {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/bases/.github/workflows/ci.yml +0 -0
  30. {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/bases/.github/workflows/publish.yml +0 -0
  31. {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/bases/.gitignore +0 -0
  32. {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/bases/.npmignore +0 -0
  33. {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/bases/.rlsbl/hooks/post-release.sh +0 -0
  34. {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/bases/.rlsbl/hooks/pre-checks.sh +0 -0
  35. {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/bases/.rlsbl/hooks/pre-release.sh +0 -0
  36. {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/bases/.rlsbl/lint/go.toml +0 -0
  37. {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/bases/.rlsbl/lint/npm.toml +0 -0
  38. {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/bases/.rlsbl/lint/python.toml +0 -0
  39. {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/bases/CHANGELOG.md +0 -0
  40. {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/bases/CLAUDE.md +0 -0
  41. {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/bases/LICENSE +0 -0
  42. {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.10.0.jsonl +0 -0
  43. {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.10.0.md +0 -0
  44. {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.11.0.jsonl +0 -0
  45. {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.11.0.md +0 -0
  46. {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.12.0.jsonl +0 -0
  47. {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.12.0.md +0 -0
  48. {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.13.0.jsonl +0 -0
  49. {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.13.0.md +0 -0
  50. {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.14.0.jsonl +0 -0
  51. {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.14.0.md +0 -0
  52. {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.4.0.jsonl +0 -0
  53. {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.4.0.md +0 -0
  54. {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.4.1.jsonl +0 -0
  55. {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.4.1.md +0 -0
  56. {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.5.0.jsonl +0 -0
  57. {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.5.0.md +0 -0
  58. {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.6.0.jsonl +0 -0
  59. {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.6.0.md +0 -0
  60. {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.6.1.jsonl +0 -0
  61. {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.6.1.md +0 -0
  62. {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.7.0.jsonl +0 -0
  63. {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.7.0.md +0 -0
  64. {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.7.1.jsonl +0 -0
  65. {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.7.1.md +0 -0
  66. {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.8.0.jsonl +0 -0
  67. {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.8.0.md +0 -0
  68. {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.8.1.jsonl +0 -0
  69. {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.8.1.md +0 -0
  70. {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.8.2.jsonl +0 -0
  71. {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.8.2.md +0 -0
  72. {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.8.3.jsonl +0 -0
  73. {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.8.3.md +0 -0
  74. {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.8.4.jsonl +0 -0
  75. {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.8.4.md +0 -0
  76. {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.8.5.jsonl +0 -0
  77. {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.8.5.md +0 -0
  78. {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.8.6.jsonl +0 -0
  79. {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.8.6.md +0 -0
  80. {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.8.7.jsonl +0 -0
  81. {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.8.7.md +0 -0
  82. {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.9.0.jsonl +0 -0
  83. {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.9.0.md +0 -0
  84. {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.9.1.jsonl +0 -0
  85. {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/0.9.1.md +0 -0
  86. {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/changes/unreleased.jsonl +0 -0
  87. {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/hashes.json +0 -0
  88. {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/hooks/post-release.sh +0 -0
  89. {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/hooks/pre-checks.sh +0 -0
  90. {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/hooks/pre-release.sh +0 -0
  91. {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/lint/go.toml +0 -0
  92. {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/lint/npm.toml +0 -0
  93. {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/lint/python.toml +0 -0
  94. {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/releases/v0.10.0.toml +0 -0
  95. {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/releases/v0.11.0.toml +0 -0
  96. {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/releases/v0.12.0.toml +0 -0
  97. {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/releases/v0.13.0.toml +0 -0
  98. {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/releases/v0.8.5.toml +0 -0
  99. {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/releases/v0.8.6.toml +0 -0
  100. {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/releases/v0.8.7.toml +0 -0
  101. {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/releases/v0.9.0.toml +0 -0
  102. {strictcli-0.14.0 → strictcli-0.15.1}/.rlsbl/releases/v0.9.1.toml +0 -0
  103. {strictcli-0.14.0 → strictcli-0.15.1}/.strictcli/schema.json +0 -0
  104. {strictcli-0.14.0 → strictcli-0.15.1}/CLAUDE.md +0 -0
  105. {strictcli-0.14.0 → strictcli-0.15.1}/LICENSE +0 -0
  106. {strictcli-0.14.0 → strictcli-0.15.1}/README.md +0 -0
  107. {strictcli-0.14.0 → strictcli-0.15.1}/index.js +0 -0
  108. {strictcli-0.14.0 → strictcli-0.15.1}/postinstall.js +0 -0
  109. {strictcli-0.14.0 → strictcli-0.15.1}/tests/test_arg_default.py +0 -0
  110. {strictcli-0.14.0 → strictcli-0.15.1}/tests/test_at_prefix.py +0 -0
  111. {strictcli-0.14.0 → strictcli-0.15.1}/tests/test_auto_version.py +0 -0
  112. {strictcli-0.14.0 → strictcli-0.15.1}/tests/test_check_command.py +0 -0
  113. {strictcli-0.14.0 → strictcli-0.15.1}/tests/test_check_discovery.py +0 -0
  114. {strictcli-0.14.0 → strictcli-0.15.1}/tests/test_check_runner.py +0 -0
  115. {strictcli-0.14.0 → strictcli-0.15.1}/tests/test_check_schema.py +0 -0
  116. {strictcli-0.14.0 → strictcli-0.15.1}/tests/test_check_types.py +0 -0
  117. {strictcli-0.14.0 → strictcli-0.15.1}/tests/test_choices.py +0 -0
  118. {strictcli-0.14.0 → strictcli-0.15.1}/tests/test_command_help_suggestion.py +0 -0
  119. {strictcli-0.14.0 → strictcli-0.15.1}/tests/test_config_file_path.py +0 -0
  120. {strictcli-0.14.0 → strictcli-0.15.1}/tests/test_deep_nesting.py +0 -0
  121. {strictcli-0.14.0 → strictcli-0.15.1}/tests/test_dependencies.py +0 -0
  122. {strictcli-0.14.0 → strictcli-0.15.1}/tests/test_deprecated.py +0 -0
  123. {strictcli-0.14.0 → strictcli-0.15.1}/tests/test_dump_schema.py +0 -0
  124. {strictcli-0.14.0 → strictcli-0.15.1}/tests/test_e2e.py +0 -0
  125. {strictcli-0.14.0 → strictcli-0.15.1}/tests/test_env.py +0 -0
  126. {strictcli-0.14.0 → strictcli-0.15.1}/tests/test_exit_codes.py +0 -0
  127. {strictcli-0.14.0 → strictcli-0.15.1}/tests/test_float_type.py +0 -0
  128. {strictcli-0.14.0 → strictcli-0.15.1}/tests/test_global_flags.py +0 -0
  129. {strictcli-0.14.0 → strictcli-0.15.1}/tests/test_help.py +0 -0
  130. {strictcli-0.14.0 → strictcli-0.15.1}/tests/test_int_type.py +0 -0
  131. {strictcli-0.14.0 → strictcli-0.15.1}/tests/test_mutex.py +0 -0
  132. {strictcli-0.14.0 → strictcli-0.15.1}/tests/test_nesting.py +0 -0
  133. {strictcli-0.14.0 → strictcli-0.15.1}/tests/test_parser.py +0 -0
  134. {strictcli-0.14.0 → strictcli-0.15.1}/tests/test_passthrough.py +0 -0
  135. {strictcli-0.14.0 → strictcli-0.15.1}/tests/test_registration.py +0 -0
  136. {strictcli-0.14.0 → strictcli-0.15.1}/tests/test_repeatable.py +0 -0
  137. {strictcli-0.14.0 → strictcli-0.15.1}/tests/test_tagdsl.py +0 -0
  138. {strictcli-0.14.0 → strictcli-0.15.1}/tests/test_tags.py +0 -0
  139. {strictcli-0.14.0 → strictcli-0.15.1}/tests/test_toml_loading.py +0 -0
  140. {strictcli-0.14.0 → strictcli-0.15.1}/tests/test_validate.py +0 -0
  141. {strictcli-0.14.0 → strictcli-0.15.1}/tests/test_variadic.py +0 -0
  142. {strictcli-0.14.0 → strictcli-0.15.1}/todo/.defer/deferred.md +0 -0
  143. {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.
@@ -0,0 +1,12 @@
1
+ {
2
+ "targets": [
3
+ "pypi",
4
+ "npm"
5
+ ],
6
+ "private": false,
7
+ "push_timeout": 300,
8
+ "pipelines": {
9
+ "pypi": {"type": "pypi", "local": false},
10
+ "npm": {"type": "npm", "local": false}
11
+ }
12
+ }
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.14.0
5
+ ## 0.15.1
6
6
 
7
- checks_embed for inline TOML data
7
+ Config show bool parity and negative values in config set
8
8
 
9
9
  <details>
10
10
  <summary>Context</summary>
11
11
 
12
- Adds checks_embed parameter as an alternative to checks_path, allowing TOML bytes to be passed directly without requiring a file on disk.
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: strictcli
3
- Version: 0.14.0
3
+ Version: 0.15.1
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
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "strictcli",
3
- "version": "0.14.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.14.0",
9
+ "version": "0.15.1",
10
10
  "hasInstallScript": true,
11
11
  "license": "MIT"
12
12
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "strictcli",
3
- "version": "0.14.0",
3
+ "version": "0.15.1",
4
4
  "description": "A strict, zero-dependency CLI framework for Python (npm wrapper)",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "strictcli"
7
- version = "0.14.0"
7
+ version = "0.15.1"
8
8
  description = "A strict, zero-dependency CLI framework for Python"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- __version__ = "0.14.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"invalid literal for int() with base 10: {s!r}")
297
- n = int(s)
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 out of range")
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) -> None:
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: dict = {}
1027
- if os.path.isfile(path):
1028
- if app_ref.config_format == "toml":
1029
- try:
1030
- with open(path, "rb") as fh:
1031
- existing = tomllib.load(fh)
1032
- except (tomllib.TOMLDecodeError, UnicodeDecodeError):
1033
- existing = {}
1034
- else:
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
- with open(path) as fh:
1037
- existing = json.loads(fh.read())
1038
- except (json.JSONDecodeError, ValueError):
1039
- existing = {}
1040
- existing[key] = value
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
- lower = env_val.lower()
1366
- if lower in ("1", "true", "yes"):
1367
- cli_set[f.name] = True
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}: expected integer, got {env_val!r} "
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}: expected integer, got {value_part!r}")
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}: expected integer, got {raw!r}")
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
- if tok in short_lookup:
1722
- f = short_lookup[tok]
1723
- if f.type is bool:
1724
- cli_set[f.name] = True
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
- raise _ParseError(f"unknown flag '{tok}'")
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
- # Unknown flag-like token
1753
- raise _ParseError(f"unknown flag '{tok}'")
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
- lower = env_val.lower()
1764
- if lower in ("1", "true", "yes"):
1765
- cli_set[f.name] = True
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}: expected integer, got {env_val!r} "
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 = False (source: default)" in r.stdout
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 ---