strictcli 0.16.1__tar.gz → 0.18.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 (158) hide show
  1. strictcli-0.18.0/.rlsbl/changes/.validated +1 -0
  2. strictcli-0.18.0/.rlsbl/changes/0.16.1.md +6 -0
  3. strictcli-0.18.0/.rlsbl/changes/0.17.0.jsonl +3 -0
  4. strictcli-0.18.0/.rlsbl/changes/0.17.0.md +9 -0
  5. strictcli-0.18.0/.rlsbl/changes/0.18.0.jsonl +4 -0
  6. strictcli-0.18.0/.rlsbl/changes/0.18.0.md +5 -0
  7. strictcli-0.18.0/.rlsbl/releases/v0.17.0.toml +8 -0
  8. strictcli-0.18.0/.rlsbl/releases/v0.18.0.toml +8 -0
  9. strictcli-0.18.0/.rlsbl/version +1 -0
  10. {strictcli-0.16.1 → strictcli-0.18.0}/CHANGELOG.md +25 -2
  11. {strictcli-0.16.1 → strictcli-0.18.0}/PKG-INFO +1 -1
  12. {strictcli-0.16.1 → strictcli-0.18.0}/package-lock.json +2 -2
  13. {strictcli-0.16.1 → strictcli-0.18.0}/package.json +1 -1
  14. {strictcli-0.16.1 → strictcli-0.18.0}/pyproject.toml +1 -1
  15. {strictcli-0.16.1 → strictcli-0.18.0}/strictcli/__init__.py +114 -31
  16. strictcli-0.18.0/tests/test_check_public_api.py +435 -0
  17. {strictcli-0.16.1 → strictcli-0.18.0}/tests/test_check_runner.py +9 -9
  18. {strictcli-0.16.1 → strictcli-0.18.0}/tests/test_config.py +4 -5
  19. {strictcli-0.16.1 → strictcli-0.18.0}/tests/test_repeatable.py +101 -0
  20. strictcli-0.18.0/todo/.done/public-check-runner-api.md +36 -0
  21. {strictcli-0.16.1 → strictcli-0.18.0}/uv.lock +1 -1
  22. strictcli-0.16.1/.rlsbl/changes/.validated +0 -1
  23. strictcli-0.16.1/.rlsbl/version +0 -1
  24. {strictcli-0.16.1 → strictcli-0.18.0}/.claude/settings.json +0 -0
  25. {strictcli-0.16.1 → strictcli-0.18.0}/.github/workflows/ci.yml +0 -0
  26. {strictcli-0.16.1 → strictcli-0.18.0}/.github/workflows/publish.yml +0 -0
  27. {strictcli-0.16.1 → strictcli-0.18.0}/.gitignore +0 -0
  28. {strictcli-0.16.1 → strictcli-0.18.0}/.npmignore +0 -0
  29. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/bases/.claude/settings.json +0 -0
  30. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/bases/.github/workflows/ci.yml +0 -0
  31. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/bases/.github/workflows/publish.yml +0 -0
  32. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/bases/.gitignore +0 -0
  33. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/bases/.npmignore +0 -0
  34. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/bases/.rlsbl/hooks/post-release.sh +0 -0
  35. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/bases/.rlsbl/hooks/pre-checks.sh +0 -0
  36. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/bases/.rlsbl/hooks/pre-release.sh +0 -0
  37. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/bases/.rlsbl/lint/go.toml +0 -0
  38. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/bases/.rlsbl/lint/npm.toml +0 -0
  39. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/bases/.rlsbl/lint/python.toml +0 -0
  40. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/bases/CHANGELOG.md +0 -0
  41. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/bases/CLAUDE.md +0 -0
  42. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/bases/LICENSE +0 -0
  43. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/changes/0.10.0.jsonl +0 -0
  44. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/changes/0.10.0.md +0 -0
  45. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/changes/0.11.0.jsonl +0 -0
  46. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/changes/0.11.0.md +0 -0
  47. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/changes/0.12.0.jsonl +0 -0
  48. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/changes/0.12.0.md +0 -0
  49. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/changes/0.13.0.jsonl +0 -0
  50. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/changes/0.13.0.md +0 -0
  51. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/changes/0.14.0.jsonl +0 -0
  52. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/changes/0.14.0.md +0 -0
  53. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/changes/0.15.0.jsonl +0 -0
  54. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/changes/0.15.0.md +0 -0
  55. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/changes/0.15.1.jsonl +0 -0
  56. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/changes/0.15.1.md +0 -0
  57. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/changes/0.16.0.jsonl +0 -0
  58. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/changes/0.16.0.md +0 -0
  59. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/changes/0.16.1.jsonl +0 -0
  60. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/changes/0.4.0.jsonl +0 -0
  61. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/changes/0.4.0.md +0 -0
  62. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/changes/0.4.1.jsonl +0 -0
  63. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/changes/0.4.1.md +0 -0
  64. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/changes/0.5.0.jsonl +0 -0
  65. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/changes/0.5.0.md +0 -0
  66. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/changes/0.6.0.jsonl +0 -0
  67. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/changes/0.6.0.md +0 -0
  68. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/changes/0.6.1.jsonl +0 -0
  69. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/changes/0.6.1.md +0 -0
  70. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/changes/0.7.0.jsonl +0 -0
  71. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/changes/0.7.0.md +0 -0
  72. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/changes/0.7.1.jsonl +0 -0
  73. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/changes/0.7.1.md +0 -0
  74. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/changes/0.8.0.jsonl +0 -0
  75. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/changes/0.8.0.md +0 -0
  76. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/changes/0.8.1.jsonl +0 -0
  77. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/changes/0.8.1.md +0 -0
  78. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/changes/0.8.2.jsonl +0 -0
  79. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/changes/0.8.2.md +0 -0
  80. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/changes/0.8.3.jsonl +0 -0
  81. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/changes/0.8.3.md +0 -0
  82. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/changes/0.8.4.jsonl +0 -0
  83. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/changes/0.8.4.md +0 -0
  84. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/changes/0.8.5.jsonl +0 -0
  85. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/changes/0.8.5.md +0 -0
  86. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/changes/0.8.6.jsonl +0 -0
  87. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/changes/0.8.6.md +0 -0
  88. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/changes/0.8.7.jsonl +0 -0
  89. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/changes/0.8.7.md +0 -0
  90. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/changes/0.9.0.jsonl +0 -0
  91. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/changes/0.9.0.md +0 -0
  92. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/changes/0.9.1.jsonl +0 -0
  93. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/changes/0.9.1.md +0 -0
  94. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/changes/unreleased.jsonl +0 -0
  95. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/config.json +0 -0
  96. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/hashes.json +0 -0
  97. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/hooks/post-release.sh +0 -0
  98. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/hooks/pre-checks.sh +0 -0
  99. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/hooks/pre-release.sh +0 -0
  100. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/lint/go.toml +0 -0
  101. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/lint/npm.toml +0 -0
  102. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/lint/python.toml +0 -0
  103. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/releases/unreleased.toml +0 -0
  104. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/releases/v0.10.0.toml +0 -0
  105. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/releases/v0.11.0.toml +0 -0
  106. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/releases/v0.12.0.toml +0 -0
  107. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/releases/v0.13.0.toml +0 -0
  108. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/releases/v0.15.0.toml +0 -0
  109. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/releases/v0.15.1.toml +0 -0
  110. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/releases/v0.16.0.toml +0 -0
  111. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/releases/v0.16.1.toml +0 -0
  112. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/releases/v0.8.5.toml +0 -0
  113. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/releases/v0.8.6.toml +0 -0
  114. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/releases/v0.8.7.toml +0 -0
  115. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/releases/v0.9.0.toml +0 -0
  116. {strictcli-0.16.1 → strictcli-0.18.0}/.rlsbl/releases/v0.9.1.toml +0 -0
  117. {strictcli-0.16.1 → strictcli-0.18.0}/.strictcli/schema.json +0 -0
  118. {strictcli-0.16.1 → strictcli-0.18.0}/CLAUDE.md +0 -0
  119. {strictcli-0.16.1 → strictcli-0.18.0}/LICENSE +0 -0
  120. {strictcli-0.16.1 → strictcli-0.18.0}/README.md +0 -0
  121. {strictcli-0.16.1 → strictcli-0.18.0}/index.js +0 -0
  122. {strictcli-0.16.1 → strictcli-0.18.0}/postinstall.js +0 -0
  123. {strictcli-0.16.1 → strictcli-0.18.0}/tests/test_arg_default.py +0 -0
  124. {strictcli-0.16.1 → strictcli-0.18.0}/tests/test_at_prefix.py +0 -0
  125. {strictcli-0.16.1 → strictcli-0.18.0}/tests/test_auto_version.py +0 -0
  126. {strictcli-0.16.1 → strictcli-0.18.0}/tests/test_check_command.py +0 -0
  127. {strictcli-0.16.1 → strictcli-0.18.0}/tests/test_check_discovery.py +0 -0
  128. {strictcli-0.16.1 → strictcli-0.18.0}/tests/test_check_schema.py +0 -0
  129. {strictcli-0.16.1 → strictcli-0.18.0}/tests/test_check_types.py +0 -0
  130. {strictcli-0.16.1 → strictcli-0.18.0}/tests/test_choices.py +0 -0
  131. {strictcli-0.16.1 → strictcli-0.18.0}/tests/test_command_help_suggestion.py +0 -0
  132. {strictcli-0.16.1 → strictcli-0.18.0}/tests/test_config_file_path.py +0 -0
  133. {strictcli-0.16.1 → strictcli-0.18.0}/tests/test_config_set_bugs.py +0 -0
  134. {strictcli-0.16.1 → strictcli-0.18.0}/tests/test_deep_nesting.py +0 -0
  135. {strictcli-0.16.1 → strictcli-0.18.0}/tests/test_dependencies.py +0 -0
  136. {strictcli-0.16.1 → strictcli-0.18.0}/tests/test_deprecated.py +0 -0
  137. {strictcli-0.16.1 → strictcli-0.18.0}/tests/test_dump_schema.py +0 -0
  138. {strictcli-0.16.1 → strictcli-0.18.0}/tests/test_e2e.py +0 -0
  139. {strictcli-0.16.1 → strictcli-0.18.0}/tests/test_env.py +0 -0
  140. {strictcli-0.16.1 → strictcli-0.18.0}/tests/test_exit_codes.py +0 -0
  141. {strictcli-0.16.1 → strictcli-0.18.0}/tests/test_float_type.py +0 -0
  142. {strictcli-0.16.1 → strictcli-0.18.0}/tests/test_global_flags.py +0 -0
  143. {strictcli-0.16.1 → strictcli-0.18.0}/tests/test_help.py +0 -0
  144. {strictcli-0.16.1 → strictcli-0.18.0}/tests/test_int_type.py +0 -0
  145. {strictcli-0.16.1 → strictcli-0.18.0}/tests/test_mutex.py +0 -0
  146. {strictcli-0.16.1 → strictcli-0.18.0}/tests/test_nesting.py +0 -0
  147. {strictcli-0.16.1 → strictcli-0.18.0}/tests/test_parser.py +0 -0
  148. {strictcli-0.16.1 → strictcli-0.18.0}/tests/test_passthrough.py +0 -0
  149. {strictcli-0.16.1 → strictcli-0.18.0}/tests/test_registration.py +0 -0
  150. {strictcli-0.16.1 → strictcli-0.18.0}/tests/test_tagdsl.py +0 -0
  151. {strictcli-0.16.1 → strictcli-0.18.0}/tests/test_tags.py +0 -0
  152. {strictcli-0.16.1 → strictcli-0.18.0}/tests/test_toml_loading.py +0 -0
  153. {strictcli-0.16.1 → strictcli-0.18.0}/tests/test_unique.py +0 -0
  154. {strictcli-0.16.1 → strictcli-0.18.0}/tests/test_utilities.py +0 -0
  155. {strictcli-0.16.1 → strictcli-0.18.0}/tests/test_validate.py +0 -0
  156. {strictcli-0.16.1 → strictcli-0.18.0}/tests/test_variadic.py +0 -0
  157. {strictcli-0.16.1 → strictcli-0.18.0}/todo/.defer/deferred.md +0 -0
  158. {strictcli-0.16.1 → strictcli-0.18.0}/todo/.done/original-idea.md +0 -0
@@ -0,0 +1 @@
1
+ 9fe0fe3a00effc0c9e39704a0bd0cc865542da83
@@ -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,9 @@
1
+ ## 0.17.0
2
+
3
+ ### Breaking
4
+
5
+ - **Breaking.** Repeatable flag defaults are now validated at registration: must be a list, `default=[]` is rejected as redundant, and element types must match the flag type.
6
+
7
+ ### Features
8
+
9
+ - **New.** Help text now displays non-empty repeatable flag defaults as `[default: a, b, c]`.
@@ -0,0 +1,4 @@
1
+ {"commits":["ab63f2e29d95b834aef6e3307d23b53e3f845afe","fc7ae2927aa12b7627c3119c818d9a7e89209e39","db1924cbd143285c857324a8fcf2110718080bfa"],"user_facing":true,"description":"**New.** Public API for running checks programmatically: `App.run_checks()`, `format_check_results()`, `format_check_results_json()`, `CheckRunResult`.","type":"feature"}
2
+ {"commits":["6407ea33c3a16a83dcd7c362cda8e98980074a73","edfcf45b0eb1ba4896fcff1d31ea9993c9b363a1","fcaae9bb7895805308a6b9299158ada7f6d45f3b","b6720d2dd1063bff0b49f6d5624608672c0cac33"],"user_facing":false}
3
+ {"commits":["9cc04830fa5b7eef0d87b249e57248415fd769a5","4e89101a5fdb8c0a6f46b62829f22e09b79ec2d1"],"user_facing":false}
4
+ {"commits":["67ee39ff3cdf4d09a9a29c0bdcb9853999878ee9"],"user_facing":false}
@@ -0,0 +1,5 @@
1
+ ## 0.18.0
2
+
3
+ ### Features
4
+
5
+ - **New.** Public API for running checks programmatically: `App.run_checks()`, `format_check_results()`, `format_check_results_json()`, `CheckRunResult`.
@@ -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 = []
@@ -0,0 +1,8 @@
1
+ # Version bump type: patch, minor, or major
2
+ bump = "minor"
3
+ # Short description of this release (required)
4
+ description = "Public API for running checks programmatically"
5
+ # Optional context explaining why these changes were made
6
+ context = "Adds App.run_checks(), format_check_results(), format_check_results_json(), and CheckRunResult — a stable public API replacing the private _filter_checks/_resolve_check_order/_run_checks/_check_format_human functions that consumers like rlsbl were forced to use."
7
+ include = ["pypi", "npm"]
8
+ exclude = []
@@ -0,0 +1 @@
1
+ 0.67.1
@@ -2,9 +2,32 @@
2
2
 
3
3
  # Changelog
4
4
 
5
- ## 0.16.1
5
+ ## 0.18.0
6
+
7
+ Public API for running checks programmatically
8
+
9
+ <details>
10
+ <summary>Context</summary>
11
+
12
+ Adds App.run_checks(), format_check_results(), format_check_results_json(), and CheckRunResult — a stable public API replacing the private _filter_checks/_resolve_check_order/_run_checks/_check_format_human functions that consumers like rlsbl were forced to use.
13
+
14
+ </details>
15
+
16
+ ### Features
17
+
18
+ - **New.** Public API for running checks programmatically: `App.run_checks()`, `format_check_results()`, `format_check_results_json()`, `CheckRunResult`.
6
19
 
7
- Error message fixes and help text improvements
20
+ ## 0.17.0
21
+
22
+ ### Breaking
23
+
24
+ - **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.
25
+
26
+ ### Features
27
+
28
+ - **New.** Help text now displays non-empty repeatable flag defaults as `[default: a, b, c]`.
29
+
30
+ ## 0.16.1
8
31
 
9
32
  ### Fixes
10
33
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: strictcli
3
- Version: 0.16.1
3
+ Version: 0.18.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.1",
3
+ "version": "0.18.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "strictcli",
9
- "version": "0.16.1",
9
+ "version": "0.18.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.1",
3
+ "version": "0.18.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.1"
7
+ version = "0.18.0"
8
8
  description = "A strict, zero-dependency CLI framework for Python"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -2,12 +2,13 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- __version__ = "0.16.1"
5
+ __version__ = "0.18.0"
6
6
 
7
7
  __all__ = [
8
8
  "App", "Flag", "Arg", "Tag", "MutexGroup", "CoRequired", "Requires",
9
9
  "Implies", "Passthrough", "DeprecatedCommand", "Result", "flag", "arg",
10
- "CheckResult", "CheckContext",
10
+ "CheckResult", "CheckContext", "CheckRunResult",
11
+ "format_check_results", "format_check_results_json",
11
12
  ]
12
13
 
13
14
  import contextlib
@@ -588,6 +589,38 @@ class Flag:
588
589
  raise ValueError(
589
590
  f'Flag "{self.name}": choice {c!r} is not of type {self.type.__name__}'
590
591
  )
592
+ # Validate repeatable flag defaults
593
+ if self.repeatable and not isinstance(self.default, _MissingSentinel):
594
+ if self.default is not None:
595
+ if not isinstance(self.default, list):
596
+ raise ValueError(
597
+ f'Flag "{self.name}": repeatable flag default must be a list'
598
+ )
599
+ if len(self.default) == 0:
600
+ raise ValueError(
601
+ f'Flag "{self.name}": explicit empty default is redundant '
602
+ f"for repeatable flags, omit the default"
603
+ )
604
+ # Validate element types
605
+ type_name = {str: "str", int: "int", float: "float"}[self.type]
606
+ for i, elem in enumerate(self.default):
607
+ if self.type is str:
608
+ if not isinstance(elem, str):
609
+ raise ValueError(
610
+ f'Flag "{self.name}": default element {i} is not of type {type_name}'
611
+ )
612
+ elif self.type is int:
613
+ if not isinstance(elem, int) or isinstance(elem, bool):
614
+ raise ValueError(
615
+ f'Flag "{self.name}": default element {i} is not of type {type_name}'
616
+ )
617
+ elif self.type is float:
618
+ if not isinstance(elem, (int, float)) or isinstance(elem, bool):
619
+ raise ValueError(
620
+ f'Flag "{self.name}": default element {i} is not of type {type_name}'
621
+ )
622
+ if isinstance(elem, int):
623
+ self.default[i] = float(elem)
591
624
  # Validate default type for int flags
592
625
  if self.type is int and not isinstance(self.default, _MissingSentinel) and self.default is not None:
593
626
  if not self.repeatable and not isinstance(self.default, int):
@@ -824,6 +857,14 @@ class CheckResult:
824
857
  raise ValueError("CheckResult.message must be a non-empty string")
825
858
 
826
859
 
860
+ @dataclass(frozen=True)
861
+ class CheckRunResult:
862
+ """A named check result returned by App.run_checks()."""
863
+
864
+ name: str
865
+ result: CheckResult
866
+
867
+
827
868
  @runtime_checkable
828
869
  class CheckContext(Protocol):
829
870
  """Minimal interface that tool-specific check contexts must satisfy."""
@@ -969,6 +1010,38 @@ class App:
969
1010
  """
970
1011
  self._check_context_factory = factory
971
1012
 
1013
+ def run_checks(
1014
+ self,
1015
+ context: CheckContext,
1016
+ *,
1017
+ tag_expr: str | None = None,
1018
+ name_glob: str | None = None,
1019
+ run_all: bool = False,
1020
+ ignore_warnings: bool = False,
1021
+ ) -> tuple[list[CheckRunResult], int]:
1022
+ """Run checks programmatically with filtering and dependency resolution.
1023
+
1024
+ Returns (results, exit_code) where results is a list of CheckRunResult
1025
+ and exit_code is 0 if all pass (or all warn with ignore_warnings), else 1.
1026
+ """
1027
+ if not self._checks_enabled:
1028
+ raise ValueError("checks are not enabled on this App")
1029
+ err = self._validate_check_registrations()
1030
+ if err:
1031
+ raise ValueError(err)
1032
+ selected = _filter_checks(self._check_defs, tag_expr, name_glob, run_all)
1033
+ if not selected:
1034
+ return ([], 0)
1035
+ order = _resolve_check_order(self._check_defs, selected)
1036
+ raw_results, exit_code = _run_checks(
1037
+ self._check_defs, order, context, ignore_warnings,
1038
+ )
1039
+ results = [
1040
+ CheckRunResult(name=name, result=result)
1041
+ for name, result in raw_results
1042
+ ]
1043
+ return (results, exit_code)
1044
+
972
1045
  def _register_check_command(self) -> None:
973
1046
  """Register the auto-generated 'check' command when checks.toml exists."""
974
1047
  app_ref = self # capture for closure
@@ -1016,14 +1089,19 @@ class App:
1016
1089
  )
1017
1090
  return 1
1018
1091
  context = app_ref._check_context_factory()
1019
- results, exit_code = _run_checks(
1020
- app_ref, order, context, ignore_warnings,
1092
+ raw_results, exit_code = _run_checks(
1093
+ app_ref._check_defs, order, context, ignore_warnings,
1021
1094
  )
1022
1095
 
1096
+ results_wrapped = [
1097
+ CheckRunResult(name=n, result=r) for n, r in raw_results
1098
+ ]
1023
1099
  if json:
1024
- _check_format_json(results)
1100
+ print(format_check_results_json(results_wrapped))
1025
1101
  else:
1026
- _check_format_human(results, verbose)
1102
+ output = format_check_results(results_wrapped, verbose)
1103
+ if output:
1104
+ print(output)
1027
1105
 
1028
1106
  return exit_code
1029
1107
 
@@ -2904,8 +2982,10 @@ def _build_flag_meta(f: Flag) -> str:
2904
2982
  if f.type is bool:
2905
2983
  meta_parts.append(f"default: {'true' if f.default else 'false'}")
2906
2984
  elif f.repeatable:
2907
- # Repeatable flags are never required; no default shown
2908
- pass
2985
+ # Repeatable flags are never required; show default only if non-empty
2986
+ if f.default:
2987
+ joined = ", ".join(_format_value_for_error(elem) for elem in f.default)
2988
+ meta_parts.append(f"default: {joined}")
2909
2989
  elif f.default is not None:
2910
2990
  meta_parts.append(f"default: {f.default}")
2911
2991
  else:
@@ -3296,7 +3376,7 @@ def _find_cycle(
3296
3376
 
3297
3377
 
3298
3378
  def _run_checks(
3299
- app: "App",
3379
+ check_defs: dict,
3300
3380
  check_names: list[str],
3301
3381
  context: CheckContext,
3302
3382
  ignore_warnings: bool,
@@ -3306,7 +3386,6 @@ def _run_checks(
3306
3386
  Returns (results_list, exit_code). exit_code is 0 if all pass (or all
3307
3387
  warn with ignore_warnings=True), 1 otherwise.
3308
3388
  """
3309
- check_defs = app._check_defs
3310
3389
  results: list[tuple[str, CheckResult]] = []
3311
3390
  failed_checks: set[str] = set()
3312
3391
  exit_code = 0
@@ -3406,39 +3485,43 @@ def _check_dry_run_mode(
3406
3485
  print(f" {i}. {name}")
3407
3486
 
3408
3487
 
3409
- def _check_format_human(
3410
- results: list[tuple[str, CheckResult]], verbose: bool,
3411
- ) -> None:
3412
- """Print check results in human-readable aligned format."""
3488
+
3489
+ def format_check_results(
3490
+ results: list[CheckRunResult], verbose: bool = False,
3491
+ ) -> str:
3492
+ """Format check results as a human-readable aligned string."""
3413
3493
  if not results:
3414
- return
3494
+ return ""
3415
3495
 
3416
- name_width = max(len(name) for name, _ in results)
3496
+ name_width = max(len(r.name) for r in results)
3497
+ lines: list[str] = []
3417
3498
 
3418
- for name, result in results:
3419
- label = _CHECK_STATUS_LABELS[result.status]
3420
- print(f"{label} {name:<{name_width}} {result.message}")
3499
+ for r in results:
3500
+ label = _CHECK_STATUS_LABELS[r.result.status]
3501
+ lines.append(f"{label} {r.name:<{name_width}} {r.result.message}")
3421
3502
 
3422
- show_details = result.details and (
3423
- verbose or result.status in ("fail", "warn", "skip")
3503
+ show_details = r.result.details and (
3504
+ verbose or r.result.status in ("fail", "warn", "skip")
3424
3505
  )
3425
3506
  if show_details:
3426
- for detail in result.details:
3427
- print(f" {detail}")
3507
+ for detail in r.result.details:
3508
+ lines.append(f" {detail}")
3509
+
3510
+ return "\n".join(lines)
3428
3511
 
3429
3512
 
3430
- def _check_format_json(results: list[tuple[str, CheckResult]]) -> None:
3431
- """Print check results as JSON."""
3513
+ def format_check_results_json(results: list[CheckRunResult]) -> str:
3514
+ """Format check results as a JSON string."""
3432
3515
  items = [
3433
3516
  {
3434
- "name": name,
3435
- "status": result.status,
3436
- "message": result.message,
3437
- "details": result.details,
3517
+ "name": r.name,
3518
+ "status": r.result.status,
3519
+ "message": r.result.message,
3520
+ "details": r.result.details if r.result.details is not None else [],
3438
3521
  }
3439
- for name, result in results
3522
+ for r in results
3440
3523
  ]
3441
- print(json.dumps(items, separators=(",", ":")))
3524
+ return json.dumps(items, separators=(",", ":"))
3442
3525
 
3443
3526
 
3444
3527
  # ---------------------------------------------------------------------------