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.
Files changed (151) hide show
  1. strictcli-0.17.0/.rlsbl/changes/.validated +1 -0
  2. strictcli-0.17.0/.rlsbl/changes/0.16.0.md +18 -0
  3. strictcli-0.17.0/.rlsbl/changes/0.16.1.jsonl +3 -0
  4. strictcli-0.17.0/.rlsbl/changes/0.16.1.md +6 -0
  5. strictcli-0.17.0/.rlsbl/changes/0.17.0.jsonl +3 -0
  6. strictcli-0.17.0/.rlsbl/releases/v0.16.1.toml +8 -0
  7. strictcli-0.17.0/.rlsbl/releases/v0.17.0.toml +8 -0
  8. {strictcli-0.16.0 → strictcli-0.17.0}/CHANGELOG.md +16 -6
  9. {strictcli-0.16.0 → strictcli-0.17.0}/PKG-INFO +1 -1
  10. {strictcli-0.16.0 → strictcli-0.17.0}/package-lock.json +2 -2
  11. {strictcli-0.16.0 → strictcli-0.17.0}/package.json +1 -1
  12. {strictcli-0.16.0 → strictcli-0.17.0}/pyproject.toml +1 -1
  13. {strictcli-0.16.0 → strictcli-0.17.0}/strictcli/__init__.py +39 -9
  14. {strictcli-0.16.0 → strictcli-0.17.0}/tests/test_config.py +17 -6
  15. {strictcli-0.16.0 → strictcli-0.17.0}/tests/test_repeatable.py +101 -0
  16. {strictcli-0.16.0 → strictcli-0.17.0}/uv.lock +1 -1
  17. strictcli-0.16.0/.rlsbl/changes/.validated +0 -1
  18. {strictcli-0.16.0 → strictcli-0.17.0}/.claude/settings.json +0 -0
  19. {strictcli-0.16.0 → strictcli-0.17.0}/.github/workflows/ci.yml +0 -0
  20. {strictcli-0.16.0 → strictcli-0.17.0}/.github/workflows/publish.yml +0 -0
  21. {strictcli-0.16.0 → strictcli-0.17.0}/.gitignore +0 -0
  22. {strictcli-0.16.0 → strictcli-0.17.0}/.npmignore +0 -0
  23. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/bases/.claude/settings.json +0 -0
  24. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/bases/.github/workflows/ci.yml +0 -0
  25. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/bases/.github/workflows/publish.yml +0 -0
  26. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/bases/.gitignore +0 -0
  27. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/bases/.npmignore +0 -0
  28. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/bases/.rlsbl/hooks/post-release.sh +0 -0
  29. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/bases/.rlsbl/hooks/pre-checks.sh +0 -0
  30. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/bases/.rlsbl/hooks/pre-release.sh +0 -0
  31. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/bases/.rlsbl/lint/go.toml +0 -0
  32. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/bases/.rlsbl/lint/npm.toml +0 -0
  33. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/bases/.rlsbl/lint/python.toml +0 -0
  34. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/bases/CHANGELOG.md +0 -0
  35. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/bases/CLAUDE.md +0 -0
  36. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/bases/LICENSE +0 -0
  37. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.10.0.jsonl +0 -0
  38. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.10.0.md +0 -0
  39. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.11.0.jsonl +0 -0
  40. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.11.0.md +0 -0
  41. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.12.0.jsonl +0 -0
  42. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.12.0.md +0 -0
  43. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.13.0.jsonl +0 -0
  44. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.13.0.md +0 -0
  45. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.14.0.jsonl +0 -0
  46. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.14.0.md +0 -0
  47. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.15.0.jsonl +0 -0
  48. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.15.0.md +0 -0
  49. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.15.1.jsonl +0 -0
  50. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.15.1.md +0 -0
  51. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.16.0.jsonl +0 -0
  52. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.4.0.jsonl +0 -0
  53. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.4.0.md +0 -0
  54. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.4.1.jsonl +0 -0
  55. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.4.1.md +0 -0
  56. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.5.0.jsonl +0 -0
  57. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.5.0.md +0 -0
  58. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.6.0.jsonl +0 -0
  59. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.6.0.md +0 -0
  60. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.6.1.jsonl +0 -0
  61. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.6.1.md +0 -0
  62. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.7.0.jsonl +0 -0
  63. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.7.0.md +0 -0
  64. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.7.1.jsonl +0 -0
  65. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.7.1.md +0 -0
  66. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.8.0.jsonl +0 -0
  67. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.8.0.md +0 -0
  68. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.8.1.jsonl +0 -0
  69. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.8.1.md +0 -0
  70. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.8.2.jsonl +0 -0
  71. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.8.2.md +0 -0
  72. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.8.3.jsonl +0 -0
  73. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.8.3.md +0 -0
  74. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.8.4.jsonl +0 -0
  75. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.8.4.md +0 -0
  76. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.8.5.jsonl +0 -0
  77. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.8.5.md +0 -0
  78. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.8.6.jsonl +0 -0
  79. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.8.6.md +0 -0
  80. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.8.7.jsonl +0 -0
  81. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.8.7.md +0 -0
  82. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.9.0.jsonl +0 -0
  83. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.9.0.md +0 -0
  84. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.9.1.jsonl +0 -0
  85. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/0.9.1.md +0 -0
  86. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/changes/unreleased.jsonl +0 -0
  87. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/config.json +0 -0
  88. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/hashes.json +0 -0
  89. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/hooks/post-release.sh +0 -0
  90. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/hooks/pre-checks.sh +0 -0
  91. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/hooks/pre-release.sh +0 -0
  92. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/lint/go.toml +0 -0
  93. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/lint/npm.toml +0 -0
  94. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/lint/python.toml +0 -0
  95. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/releases/unreleased.toml +0 -0
  96. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/releases/v0.10.0.toml +0 -0
  97. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/releases/v0.11.0.toml +0 -0
  98. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/releases/v0.12.0.toml +0 -0
  99. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/releases/v0.13.0.toml +0 -0
  100. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/releases/v0.15.0.toml +0 -0
  101. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/releases/v0.15.1.toml +0 -0
  102. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/releases/v0.16.0.toml +0 -0
  103. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/releases/v0.8.5.toml +0 -0
  104. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/releases/v0.8.6.toml +0 -0
  105. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/releases/v0.8.7.toml +0 -0
  106. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/releases/v0.9.0.toml +0 -0
  107. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/releases/v0.9.1.toml +0 -0
  108. {strictcli-0.16.0 → strictcli-0.17.0}/.rlsbl/version +0 -0
  109. {strictcli-0.16.0 → strictcli-0.17.0}/.strictcli/schema.json +0 -0
  110. {strictcli-0.16.0 → strictcli-0.17.0}/CLAUDE.md +0 -0
  111. {strictcli-0.16.0 → strictcli-0.17.0}/LICENSE +0 -0
  112. {strictcli-0.16.0 → strictcli-0.17.0}/README.md +0 -0
  113. {strictcli-0.16.0 → strictcli-0.17.0}/index.js +0 -0
  114. {strictcli-0.16.0 → strictcli-0.17.0}/postinstall.js +0 -0
  115. {strictcli-0.16.0 → strictcli-0.17.0}/tests/test_arg_default.py +0 -0
  116. {strictcli-0.16.0 → strictcli-0.17.0}/tests/test_at_prefix.py +0 -0
  117. {strictcli-0.16.0 → strictcli-0.17.0}/tests/test_auto_version.py +0 -0
  118. {strictcli-0.16.0 → strictcli-0.17.0}/tests/test_check_command.py +0 -0
  119. {strictcli-0.16.0 → strictcli-0.17.0}/tests/test_check_discovery.py +0 -0
  120. {strictcli-0.16.0 → strictcli-0.17.0}/tests/test_check_runner.py +0 -0
  121. {strictcli-0.16.0 → strictcli-0.17.0}/tests/test_check_schema.py +0 -0
  122. {strictcli-0.16.0 → strictcli-0.17.0}/tests/test_check_types.py +0 -0
  123. {strictcli-0.16.0 → strictcli-0.17.0}/tests/test_choices.py +0 -0
  124. {strictcli-0.16.0 → strictcli-0.17.0}/tests/test_command_help_suggestion.py +0 -0
  125. {strictcli-0.16.0 → strictcli-0.17.0}/tests/test_config_file_path.py +0 -0
  126. {strictcli-0.16.0 → strictcli-0.17.0}/tests/test_config_set_bugs.py +0 -0
  127. {strictcli-0.16.0 → strictcli-0.17.0}/tests/test_deep_nesting.py +0 -0
  128. {strictcli-0.16.0 → strictcli-0.17.0}/tests/test_dependencies.py +0 -0
  129. {strictcli-0.16.0 → strictcli-0.17.0}/tests/test_deprecated.py +0 -0
  130. {strictcli-0.16.0 → strictcli-0.17.0}/tests/test_dump_schema.py +0 -0
  131. {strictcli-0.16.0 → strictcli-0.17.0}/tests/test_e2e.py +0 -0
  132. {strictcli-0.16.0 → strictcli-0.17.0}/tests/test_env.py +0 -0
  133. {strictcli-0.16.0 → strictcli-0.17.0}/tests/test_exit_codes.py +0 -0
  134. {strictcli-0.16.0 → strictcli-0.17.0}/tests/test_float_type.py +0 -0
  135. {strictcli-0.16.0 → strictcli-0.17.0}/tests/test_global_flags.py +0 -0
  136. {strictcli-0.16.0 → strictcli-0.17.0}/tests/test_help.py +0 -0
  137. {strictcli-0.16.0 → strictcli-0.17.0}/tests/test_int_type.py +0 -0
  138. {strictcli-0.16.0 → strictcli-0.17.0}/tests/test_mutex.py +0 -0
  139. {strictcli-0.16.0 → strictcli-0.17.0}/tests/test_nesting.py +0 -0
  140. {strictcli-0.16.0 → strictcli-0.17.0}/tests/test_parser.py +0 -0
  141. {strictcli-0.16.0 → strictcli-0.17.0}/tests/test_passthrough.py +0 -0
  142. {strictcli-0.16.0 → strictcli-0.17.0}/tests/test_registration.py +0 -0
  143. {strictcli-0.16.0 → strictcli-0.17.0}/tests/test_tagdsl.py +0 -0
  144. {strictcli-0.16.0 → strictcli-0.17.0}/tests/test_tags.py +0 -0
  145. {strictcli-0.16.0 → strictcli-0.17.0}/tests/test_toml_loading.py +0 -0
  146. {strictcli-0.16.0 → strictcli-0.17.0}/tests/test_unique.py +0 -0
  147. {strictcli-0.16.0 → strictcli-0.17.0}/tests/test_utilities.py +0 -0
  148. {strictcli-0.16.0 → strictcli-0.17.0}/tests/test_validate.py +0 -0
  149. {strictcli-0.16.0 → strictcli-0.17.0}/tests/test_variadic.py +0 -0
  150. {strictcli-0.16.0 → strictcli-0.17.0}/todo/.defer/deferred.md +0 -0
  151. {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,6 @@
1
+ ## 0.16.1
2
+
3
+ ### Fixes
4
+
5
+ - **Fix.** `config set` mutex error messages now include the `config set:` prefix for consistency.
6
+ - **Fix.** Config set help text now mentions backslash escaping for values containing commas in repeatable flags.
@@ -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.16.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
- unique and env_separator flag fields, config array coercion, config set repeatable support
13
+ ### Features
14
+
15
+ - **New.** Help text now displays non-empty repeatable flag defaults as `[default: a, b, c]`.
8
16
 
9
- <details>
10
- <summary>Context</summary>
17
+ ## 0.16.1
11
18
 
12
- Breaking: config show now requires --plain or --json. New features: unique field enforces no duplicate values on repeatable flags, env_separator controls how env vars are split into arrays, config arrays are coerced to declared types, config set supports repeatable flags with --clear and --default.
19
+ ### Fixes
13
20
 
14
- </details>
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: strictcli
3
- Version: 0.16.0
3
+ Version: 0.17.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
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "strictcli",
3
- "version": "0.16.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.16.0",
9
+ "version": "0.17.0",
10
10
  "hasInstallScript": true,
11
11
  "license": "MIT"
12
12
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "strictcli",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
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.16.0"
7
+ version = "0.17.0"
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.16.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; no default shown
2912
- pass
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, default=[])
1119
+ unique=False)
1121
1120
  @strictcli.flag("rates", type=float, help="rates", repeatable=True,
1122
- unique=False, default=[])
1121
+ unique=False)
1123
1122
  @strictcli.flag("ids", type=int, help="unique ids", repeatable=True,
1124
- unique=True, default=[])
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
@@ -232,7 +232,7 @@ wheels = [
232
232
 
233
233
  [[package]]
234
234
  name = "strictcli"
235
- version = "0.16.0"
235
+ version = "0.17.0"
236
236
  source = { editable = "." }
237
237
 
238
238
  [package.dev-dependencies]
@@ -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