strictcli 0.13.0__tar.gz → 0.15.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- strictcli-0.15.0/.github/workflows/publish.yml +27 -0
- strictcli-0.15.0/.npmignore +34 -0
- strictcli-0.15.0/.rlsbl/bases/.github/workflows/publish.yml +27 -0
- strictcli-0.15.0/.rlsbl/bases/.npmignore +34 -0
- strictcli-0.15.0/.rlsbl/changes/.validated +1 -0
- strictcli-0.15.0/.rlsbl/changes/0.13.0.md +5 -0
- strictcli-0.15.0/.rlsbl/changes/0.14.0.jsonl +3 -0
- strictcli-0.15.0/.rlsbl/changes/0.14.0.md +5 -0
- strictcli-0.15.0/.rlsbl/changes/0.15.0.jsonl +6 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/hashes.json +3 -2
- strictcli-0.15.0/.rlsbl/releases/v0.15.0.toml +8 -0
- strictcli-0.15.0/.rlsbl/version +1 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/CHANGELOG.md +25 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/PKG-INFO +1 -1
- {strictcli-0.13.0 → strictcli-0.15.0}/package-lock.json +2 -2
- {strictcli-0.13.0 → strictcli-0.15.0}/package.json +1 -1
- {strictcli-0.13.0 → strictcli-0.15.0}/pyproject.toml +1 -1
- {strictcli-0.13.0 → strictcli-0.15.0}/strictcli/__init__.py +97 -42
- {strictcli-0.13.0 → strictcli-0.15.0}/tests/test_check_discovery.py +51 -0
- strictcli-0.15.0/tests/test_config_set_bugs.py +242 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/uv.lock +1 -1
- strictcli-0.13.0/.github/workflows/publish.yml +0 -32
- strictcli-0.13.0/.rlsbl/bases/.github/workflows/publish.yml +0 -32
- strictcli-0.13.0/.rlsbl/changes/.validated +0 -1
- strictcli-0.13.0/.rlsbl/version +0 -1
- {strictcli-0.13.0 → strictcli-0.15.0}/.claude/settings.json +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/.github/workflows/ci.yml +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/.gitignore +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/bases/.claude/settings.json +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/bases/.github/workflows/ci.yml +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/bases/.gitignore +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/bases/.rlsbl/hooks/post-release.sh +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/bases/.rlsbl/hooks/pre-checks.sh +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/bases/.rlsbl/hooks/pre-release.sh +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/bases/.rlsbl/lint/go.toml +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/bases/.rlsbl/lint/npm.toml +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/bases/.rlsbl/lint/python.toml +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/bases/CHANGELOG.md +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/bases/CLAUDE.md +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/bases/LICENSE +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.10.0.jsonl +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.10.0.md +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.11.0.jsonl +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.11.0.md +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.12.0.jsonl +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.12.0.md +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.13.0.jsonl +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.4.0.jsonl +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.4.0.md +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.4.1.jsonl +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.4.1.md +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.5.0.jsonl +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.5.0.md +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.6.0.jsonl +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.6.0.md +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.6.1.jsonl +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.6.1.md +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.7.0.jsonl +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.7.0.md +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.7.1.jsonl +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.7.1.md +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.8.0.jsonl +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.8.0.md +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.8.1.jsonl +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.8.1.md +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.8.2.jsonl +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.8.2.md +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.8.3.jsonl +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.8.3.md +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.8.4.jsonl +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.8.4.md +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.8.5.jsonl +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.8.5.md +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.8.6.jsonl +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.8.6.md +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.8.7.jsonl +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.8.7.md +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.9.0.jsonl +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.9.0.md +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.9.1.jsonl +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.9.1.md +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/unreleased.jsonl +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/config.json +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/hooks/post-release.sh +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/hooks/pre-checks.sh +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/hooks/pre-release.sh +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/lint/go.toml +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/lint/npm.toml +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/lint/python.toml +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/releases/unreleased.toml +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/releases/v0.10.0.toml +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/releases/v0.11.0.toml +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/releases/v0.12.0.toml +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/releases/v0.13.0.toml +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/releases/v0.8.5.toml +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/releases/v0.8.6.toml +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/releases/v0.8.7.toml +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/releases/v0.9.0.toml +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/releases/v0.9.1.toml +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/.strictcli/schema.json +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/CLAUDE.md +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/LICENSE +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/README.md +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/index.js +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/postinstall.js +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/tests/test_arg_default.py +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/tests/test_at_prefix.py +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/tests/test_auto_version.py +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/tests/test_check_command.py +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/tests/test_check_runner.py +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/tests/test_check_schema.py +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/tests/test_check_types.py +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/tests/test_choices.py +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/tests/test_command_help_suggestion.py +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/tests/test_config.py +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/tests/test_config_file_path.py +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/tests/test_deep_nesting.py +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/tests/test_dependencies.py +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/tests/test_deprecated.py +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/tests/test_dump_schema.py +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/tests/test_e2e.py +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/tests/test_env.py +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/tests/test_exit_codes.py +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/tests/test_float_type.py +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/tests/test_global_flags.py +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/tests/test_help.py +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/tests/test_int_type.py +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/tests/test_mutex.py +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/tests/test_nesting.py +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/tests/test_parser.py +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/tests/test_passthrough.py +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/tests/test_registration.py +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/tests/test_repeatable.py +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/tests/test_tagdsl.py +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/tests/test_tags.py +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/tests/test_toml_loading.py +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/tests/test_validate.py +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/tests/test_variadic.py +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/todo/.defer/deferred.md +0 -0
- {strictcli-0.13.0 → strictcli-0.15.0}/todo/.done/original-idea.md +0 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
on:
|
|
3
|
+
release:
|
|
4
|
+
types: [published]
|
|
5
|
+
workflow_dispatch:
|
|
6
|
+
permissions:
|
|
7
|
+
contents: read
|
|
8
|
+
id-token: write
|
|
9
|
+
jobs:
|
|
10
|
+
pypi:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v6
|
|
14
|
+
- uses: astral-sh/setup-uv@v7
|
|
15
|
+
- run: uv build
|
|
16
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
17
|
+
npm:
|
|
18
|
+
runs-on: ubuntu-latest
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v6
|
|
21
|
+
- uses: actions/setup-node@v6
|
|
22
|
+
with:
|
|
23
|
+
node-version: 24
|
|
24
|
+
registry-url: https://registry.npmjs.org
|
|
25
|
+
- run: npm publish --provenance --access public
|
|
26
|
+
env:
|
|
27
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Development and CI
|
|
2
|
+
tests/
|
|
3
|
+
test/
|
|
4
|
+
__tests__/
|
|
5
|
+
.github/
|
|
6
|
+
scripts/
|
|
7
|
+
docs/
|
|
8
|
+
todo/
|
|
9
|
+
e2e/
|
|
10
|
+
|
|
11
|
+
# rlsbl metadata
|
|
12
|
+
.rlsbl/
|
|
13
|
+
.rlsbl-monorepo/
|
|
14
|
+
|
|
15
|
+
# Python artifacts (for multi-target projects)
|
|
16
|
+
__pycache__/
|
|
17
|
+
*.pyc
|
|
18
|
+
*.py
|
|
19
|
+
*.toml
|
|
20
|
+
.venv/
|
|
21
|
+
|
|
22
|
+
# Build/cache
|
|
23
|
+
coverage/
|
|
24
|
+
.nyc_output/
|
|
25
|
+
|
|
26
|
+
# Editor/OS
|
|
27
|
+
.DS_Store
|
|
28
|
+
*.log
|
|
29
|
+
.env
|
|
30
|
+
.env.local
|
|
31
|
+
|
|
32
|
+
# Selfdoc
|
|
33
|
+
.selfdoc/
|
|
34
|
+
selfdoc.json
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
on:
|
|
3
|
+
release:
|
|
4
|
+
types: [published]
|
|
5
|
+
workflow_dispatch:
|
|
6
|
+
permissions:
|
|
7
|
+
contents: read
|
|
8
|
+
id-token: write
|
|
9
|
+
jobs:
|
|
10
|
+
pypi:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v6
|
|
14
|
+
- uses: astral-sh/setup-uv@v7
|
|
15
|
+
- run: uv build
|
|
16
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
17
|
+
npm:
|
|
18
|
+
runs-on: ubuntu-latest
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v6
|
|
21
|
+
- uses: actions/setup-node@v6
|
|
22
|
+
with:
|
|
23
|
+
node-version: 24
|
|
24
|
+
registry-url: https://registry.npmjs.org
|
|
25
|
+
- run: npm publish --provenance --access public
|
|
26
|
+
env:
|
|
27
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Development and CI
|
|
2
|
+
tests/
|
|
3
|
+
test/
|
|
4
|
+
__tests__/
|
|
5
|
+
.github/
|
|
6
|
+
scripts/
|
|
7
|
+
docs/
|
|
8
|
+
todo/
|
|
9
|
+
e2e/
|
|
10
|
+
|
|
11
|
+
# rlsbl metadata
|
|
12
|
+
.rlsbl/
|
|
13
|
+
.rlsbl-monorepo/
|
|
14
|
+
|
|
15
|
+
# Python artifacts (for multi-target projects)
|
|
16
|
+
__pycache__/
|
|
17
|
+
*.pyc
|
|
18
|
+
*.py
|
|
19
|
+
*.toml
|
|
20
|
+
.venv/
|
|
21
|
+
|
|
22
|
+
# Build/cache
|
|
23
|
+
coverage/
|
|
24
|
+
.nyc_output/
|
|
25
|
+
|
|
26
|
+
# Editor/OS
|
|
27
|
+
.DS_Store
|
|
28
|
+
*.log
|
|
29
|
+
.env
|
|
30
|
+
.env.local
|
|
31
|
+
|
|
32
|
+
# Selfdoc
|
|
33
|
+
.selfdoc/
|
|
34
|
+
selfdoc.json
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
a6dff298b7a2dad7fa928b088eeb4f3f9c0b2cf5
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
{"commits":["f2f2de83edc0b7fe24f46744201e8be1407f28a3"],"user_facing":false}
|
|
2
|
+
{"commits":["a2e1d64fb5f695169fafa63e5d1b69fe5c4e043d"],"user_facing":true,"description":"**New feature.** `checks_embed` parameter accepts raw TOML bytes, enabling inline checks configuration without a file on disk.","type":"feature"}
|
|
3
|
+
{"commits":["09799755dc2ef93a088f739a46001102bcb2aa5e"],"user_facing":false}
|
|
@@ -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}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
".github/workflows/ci.yml": "b4e075171b131d25f9cce55a3e3a30732e95a2a794af00f354ee3200626d6a04",
|
|
3
|
-
".github/workflows/publish.yml": "
|
|
3
|
+
".github/workflows/publish.yml": "e77dcd0b5d1c1abae5424e5f3b73862f44f06b5d46209cbe71bc04d6baa6b5be",
|
|
4
4
|
"CHANGELOG.md": "7a6ec46141007c18090a0ff325e2dd68cd51588c4e0b5af7b580365e5eaaca85",
|
|
5
5
|
".gitignore": "c89d76c6c43e7f0897b3472a15b7c0103e0be5b3a04c2b44e585fa48bdcda2bb",
|
|
6
6
|
"LICENSE": "e958892abc1dd5d991fca6d528e695d335f25773d373d2e0be5f4a9507d8f8d5",
|
|
@@ -11,5 +11,6 @@
|
|
|
11
11
|
".rlsbl/hooks/pre-checks.sh": "60c57d4563263124530e6f33aca77550ea2161ad3b33b3ba6ad1c03fea9eb6e2",
|
|
12
12
|
".rlsbl/lint/python.toml": "9cfbcef2e010d5fad243437bf2fd8df54a1fae70ee0762f9d11da70fe207501c",
|
|
13
13
|
".rlsbl/lint/go.toml": "807d5cf760baad6d571a5bfd43cb163efe356f7462ba9f1918d72de45f62339f",
|
|
14
|
-
".rlsbl/lint/npm.toml": "baa36014e65923d8cc5c8b83c77fa651da79351627570476402da66840027645"
|
|
14
|
+
".rlsbl/lint/npm.toml": "baa36014e65923d8cc5c8b83c77fa651da79351627570476402da66840027645",
|
|
15
|
+
".npmignore": "7c0eea9785c0318aeb39bf8b670ce8384f4a5b812fde69ce23d7d018057325a3"
|
|
15
16
|
}
|
|
@@ -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 @@
|
|
|
1
|
+
0.60.1
|
|
@@ -2,6 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
# Changelog
|
|
4
4
|
|
|
5
|
+
## 0.15.0
|
|
6
|
+
|
|
7
|
+
Config set type coercion and key validation
|
|
8
|
+
|
|
9
|
+
<details>
|
|
10
|
+
<summary>Context</summary>
|
|
11
|
+
|
|
12
|
+
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.
|
|
13
|
+
|
|
14
|
+
</details>
|
|
15
|
+
|
|
16
|
+
### Breaking
|
|
17
|
+
|
|
18
|
+
- **Breaking.** `config set` now validates keys against registered flags and rejects unknown keys.
|
|
19
|
+
|
|
20
|
+
### Features
|
|
21
|
+
|
|
22
|
+
- **New feature.** `config set` coerces values to the flag's declared type (int, bool, float) before writing to config.
|
|
23
|
+
|
|
24
|
+
## 0.14.0
|
|
25
|
+
|
|
26
|
+
### Features
|
|
27
|
+
|
|
28
|
+
- **New feature.** `checks_embed` parameter accepts raw TOML bytes, enabling inline checks configuration without a file on disk.
|
|
29
|
+
|
|
5
30
|
## 0.13.0
|
|
6
31
|
|
|
7
32
|
### Features
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "strictcli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "strictcli",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.15.0",
|
|
10
10
|
"hasInstallScript": true,
|
|
11
11
|
"license": "MIT"
|
|
12
12
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
__version__ = "0.
|
|
5
|
+
__version__ = "0.15.0"
|
|
6
6
|
|
|
7
7
|
__all__ = [
|
|
8
8
|
"App", "Flag", "Arg", "Tag", "MutexGroup", "CoRequired", "Requires",
|
|
@@ -130,37 +130,32 @@ _CHECK_REQUIRED_FIELDS = {"tags", "severity", "fast", "pure", "needs_network", "
|
|
|
130
130
|
_CHECK_VALID_SEVERITIES = {"error", "warn"}
|
|
131
131
|
|
|
132
132
|
|
|
133
|
-
def
|
|
134
|
-
"""Parse and validate
|
|
133
|
+
def _parse_checks_toml(data: bytes) -> tuple[str, dict[str, _CheckDef]]:
|
|
134
|
+
"""Parse and validate checks TOML data, returning (app_name, check_defs).
|
|
135
135
|
|
|
136
|
-
Raises ValueError on any schema violation
|
|
136
|
+
Raises ValueError on any schema violation or invalid TOML.
|
|
137
137
|
"""
|
|
138
|
-
path = Path(path)
|
|
139
138
|
try:
|
|
140
|
-
|
|
141
|
-
except OSError as exc:
|
|
142
|
-
raise ValueError(f"checks.toml: {exc}") from exc
|
|
143
|
-
try:
|
|
144
|
-
data = tomllib.loads(raw.decode())
|
|
139
|
+
parsed = tomllib.loads(data.decode())
|
|
145
140
|
except (tomllib.TOMLDecodeError, UnicodeDecodeError) as exc:
|
|
146
141
|
raise ValueError(f"checks.toml: {exc}") from exc
|
|
147
142
|
|
|
148
143
|
# Only "app" and [checks] are allowed at the top level
|
|
149
|
-
for key in
|
|
144
|
+
for key in parsed:
|
|
150
145
|
if key not in ("app", "checks"):
|
|
151
146
|
raise ValueError(f'checks.toml: unknown top-level key "{key}"')
|
|
152
147
|
|
|
153
148
|
# Validate required "app" field
|
|
154
|
-
if "app" not in
|
|
149
|
+
if "app" not in parsed:
|
|
155
150
|
raise ValueError('checks.toml: missing required top-level key "app"')
|
|
156
|
-
if not isinstance(
|
|
151
|
+
if not isinstance(parsed["app"], str) or not parsed["app"]:
|
|
157
152
|
raise ValueError('checks.toml: "app" must be a non-empty string')
|
|
158
|
-
app_name =
|
|
153
|
+
app_name = parsed["app"]
|
|
159
154
|
|
|
160
|
-
if "checks" not in
|
|
155
|
+
if "checks" not in parsed:
|
|
161
156
|
return (app_name, {})
|
|
162
157
|
|
|
163
|
-
checks_section =
|
|
158
|
+
checks_section = parsed["checks"]
|
|
164
159
|
if not isinstance(checks_section, dict):
|
|
165
160
|
raise ValueError("checks.toml: [checks] must be a table")
|
|
166
161
|
|
|
@@ -253,6 +248,19 @@ def _load_checks_toml(path: str | Path) -> tuple[str, dict[str, _CheckDef]]:
|
|
|
253
248
|
return (app_name, result)
|
|
254
249
|
|
|
255
250
|
|
|
251
|
+
def _load_checks_toml(path: str | Path) -> tuple[str, dict[str, _CheckDef]]:
|
|
252
|
+
"""Read and parse a checks.toml file, returning (app_name, check_defs).
|
|
253
|
+
|
|
254
|
+
Raises ValueError on any file error, schema violation, or invalid TOML.
|
|
255
|
+
"""
|
|
256
|
+
path = Path(path)
|
|
257
|
+
try:
|
|
258
|
+
raw = path.read_bytes()
|
|
259
|
+
except OSError as exc:
|
|
260
|
+
raise ValueError(f"checks.toml: {exc}") from exc
|
|
261
|
+
return _parse_checks_toml(raw)
|
|
262
|
+
|
|
263
|
+
|
|
256
264
|
class _HelpRequested(Exception):
|
|
257
265
|
"""Raised when --help or -h is encountered."""
|
|
258
266
|
|
|
@@ -277,6 +285,21 @@ class _ParseError(Exception):
|
|
|
277
285
|
self.command_prefix = command_prefix
|
|
278
286
|
|
|
279
287
|
|
|
288
|
+
def _strict_bool(s: str) -> bool:
|
|
289
|
+
"""Parse a boolean string strictly.
|
|
290
|
+
|
|
291
|
+
Accepts: 1, true, yes (case-insensitive) -> True
|
|
292
|
+
Accepts: 0, false, no (case-insensitive) -> False
|
|
293
|
+
Everything else raises ValueError.
|
|
294
|
+
"""
|
|
295
|
+
lower = s.lower()
|
|
296
|
+
if lower in ("1", "true", "yes"):
|
|
297
|
+
return True
|
|
298
|
+
if lower in ("0", "false", "no"):
|
|
299
|
+
return False
|
|
300
|
+
raise ValueError(f"expected boolean, got '{s}'")
|
|
301
|
+
|
|
302
|
+
|
|
280
303
|
def _strict_int(s: str) -> int:
|
|
281
304
|
"""Parse an integer string strictly -- no leading/trailing whitespace allowed.
|
|
282
305
|
|
|
@@ -675,6 +698,7 @@ class App:
|
|
|
675
698
|
config_path: str | None = None
|
|
676
699
|
config_format: str = "json"
|
|
677
700
|
checks_path: str | Path | None = None
|
|
701
|
+
checks_embed: bytes | None = None
|
|
678
702
|
flags: list[Flag] = field(default_factory=list)
|
|
679
703
|
_commands: dict[str, Command] = field(default_factory=dict)
|
|
680
704
|
_groups: dict[str, Group] = field(default_factory=dict)
|
|
@@ -712,6 +736,8 @@ class App:
|
|
|
712
736
|
self._register_config_group()
|
|
713
737
|
# Discover checks TOML
|
|
714
738
|
self._check_context_factory: Callable | None = None
|
|
739
|
+
if self.checks_path is not None and self.checks_embed is not None:
|
|
740
|
+
raise ValueError("cannot use both checks_path and checks_embed")
|
|
715
741
|
if self.checks_path is not None:
|
|
716
742
|
checks_toml_path = Path(self.checks_path).resolve()
|
|
717
743
|
if not checks_toml_path.is_file():
|
|
@@ -723,6 +749,14 @@ class App:
|
|
|
723
749
|
)
|
|
724
750
|
self._checks_enabled = True
|
|
725
751
|
self._register_check_command()
|
|
752
|
+
elif self.checks_embed is not None:
|
|
753
|
+
app_name, self._check_defs = _parse_checks_toml(self.checks_embed)
|
|
754
|
+
if app_name != self.name:
|
|
755
|
+
raise ValueError(
|
|
756
|
+
f'checks.toml: app "{app_name}" does not match app name "{self.name}"'
|
|
757
|
+
)
|
|
758
|
+
self._checks_enabled = True
|
|
759
|
+
self._register_check_command()
|
|
726
760
|
else:
|
|
727
761
|
self._check_defs: dict[str, _CheckDef] = {}
|
|
728
762
|
self._checks_enabled = False
|
|
@@ -995,7 +1029,7 @@ class App:
|
|
|
995
1029
|
)
|
|
996
1030
|
|
|
997
1031
|
# config set
|
|
998
|
-
def _config_set_handler(key, value, **_kw) ->
|
|
1032
|
+
def _config_set_handler(key, value, **_kw) -> int:
|
|
999
1033
|
path = _config_path(
|
|
1000
1034
|
app_ref.name,
|
|
1001
1035
|
override=app_ref.config_path,
|
|
@@ -1004,26 +1038,53 @@ class App:
|
|
|
1004
1038
|
dir_path = os.path.dirname(path)
|
|
1005
1039
|
os.makedirs(dir_path, exist_ok=True)
|
|
1006
1040
|
# Read existing config
|
|
1007
|
-
existing
|
|
1008
|
-
|
|
1009
|
-
|
|
1041
|
+
existing = _load_config(
|
|
1042
|
+
app_ref.name,
|
|
1043
|
+
config_path_override=app_ref.config_path,
|
|
1044
|
+
config_format=app_ref.config_format,
|
|
1045
|
+
)
|
|
1046
|
+
|
|
1047
|
+
# Look up the key against registered flags
|
|
1048
|
+
all_flags = app_ref._collect_all_flags()
|
|
1049
|
+
matched_flag = None
|
|
1050
|
+
for f in all_flags:
|
|
1051
|
+
if _flag_param_name(f.name) == key:
|
|
1052
|
+
matched_flag = f
|
|
1053
|
+
break
|
|
1054
|
+
if matched_flag is None:
|
|
1055
|
+
print(f"config set: unknown key '{key}'", file=sys.stderr)
|
|
1056
|
+
return 1
|
|
1057
|
+
|
|
1058
|
+
# Coerce the string value to the flag's type
|
|
1059
|
+
try:
|
|
1060
|
+
if matched_flag.type == bool:
|
|
1061
|
+
typed_value = _strict_bool(value)
|
|
1062
|
+
elif matched_flag.type == int:
|
|
1010
1063
|
try:
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
else:
|
|
1064
|
+
typed_value = _strict_int(value)
|
|
1065
|
+
except ValueError:
|
|
1066
|
+
raise ValueError(f"expected integer, got '{value}'")
|
|
1067
|
+
elif matched_flag.type == float:
|
|
1016
1068
|
try:
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1069
|
+
typed_value = _strict_float(value)
|
|
1070
|
+
except ValueError as fe:
|
|
1071
|
+
msg = str(fe)
|
|
1072
|
+
if msg in ("NaN is not allowed", "Inf is not allowed"):
|
|
1073
|
+
raise
|
|
1074
|
+
raise ValueError(f"expected float, got '{value}'") from fe
|
|
1075
|
+
else: # str
|
|
1076
|
+
typed_value = value
|
|
1077
|
+
except ValueError as e:
|
|
1078
|
+
print(f"config set: key '{key}': {e}", file=sys.stderr)
|
|
1079
|
+
return 1
|
|
1080
|
+
|
|
1081
|
+
existing[key] = typed_value
|
|
1022
1082
|
if app_ref.config_format == "toml":
|
|
1023
1083
|
_write_toml_flat(existing, path)
|
|
1024
1084
|
else:
|
|
1025
1085
|
with open(path, "w") as fh:
|
|
1026
1086
|
fh.write(json.dumps(existing, indent=2) + "\n")
|
|
1087
|
+
return 0
|
|
1027
1088
|
|
|
1028
1089
|
config_grp.commands["set"] = Command(
|
|
1029
1090
|
name="set",
|
|
@@ -1343,12 +1404,9 @@ class App:
|
|
|
1343
1404
|
env_val = os.environ.get(f.env)
|
|
1344
1405
|
if env_val is not None:
|
|
1345
1406
|
if f.type is bool:
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
elif lower in ("0", "false", "no"):
|
|
1350
|
-
cli_set[f.name] = False
|
|
1351
|
-
else:
|
|
1407
|
+
try:
|
|
1408
|
+
cli_set[f.name] = _strict_bool(env_val)
|
|
1409
|
+
except ValueError:
|
|
1352
1410
|
raise _ParseError(
|
|
1353
1411
|
f"invalid boolean value {env_val!r} for env var "
|
|
1354
1412
|
f"'{f.env}' (flag '--{f.name}')"
|
|
@@ -1741,12 +1799,9 @@ def _parse_command(
|
|
|
1741
1799
|
env_val = os.environ.get(f.env)
|
|
1742
1800
|
if env_val is not None:
|
|
1743
1801
|
if f.type is bool:
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
elif lower in ("0", "false", "no"):
|
|
1748
|
-
cli_set[f.name] = False
|
|
1749
|
-
else:
|
|
1802
|
+
try:
|
|
1803
|
+
cli_set[f.name] = _strict_bool(env_val)
|
|
1804
|
+
except ValueError:
|
|
1750
1805
|
raise _ParseError(
|
|
1751
1806
|
f"invalid boolean value {env_val!r} for env var "
|
|
1752
1807
|
f"'{f.env}' (flag '--{f.name}')"
|
|
@@ -293,3 +293,54 @@ depends_on = []
|
|
|
293
293
|
name="testapp", version="1.0.0", help="test app",
|
|
294
294
|
checks_path=str(toml_file),
|
|
295
295
|
)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
class TestChecksEmbed:
|
|
299
|
+
def test_checks_embed_enables_checks(self, tmp_path, monkeypatch):
|
|
300
|
+
"""checks_embed with valid TOML bytes enables checks."""
|
|
301
|
+
monkeypatch.chdir(tmp_path)
|
|
302
|
+
|
|
303
|
+
app = strictcli.App(
|
|
304
|
+
name="testapp", version="1.0.0", help="test app",
|
|
305
|
+
checks_embed=VALID_TOML.encode(),
|
|
306
|
+
)
|
|
307
|
+
assert app._checks_enabled is True
|
|
308
|
+
assert "lint-code" in app._check_defs
|
|
309
|
+
assert "check-deps" in app._check_defs
|
|
310
|
+
|
|
311
|
+
def test_checks_embed_and_checks_path_raises(self, tmp_path, monkeypatch):
|
|
312
|
+
"""Using both checks_path and checks_embed raises ValueError."""
|
|
313
|
+
monkeypatch.chdir(tmp_path)
|
|
314
|
+
toml_file = tmp_path / "checks.toml"
|
|
315
|
+
toml_file.write_text(VALID_TOML)
|
|
316
|
+
|
|
317
|
+
with pytest.raises(ValueError, match="cannot use both checks_path and checks_embed"):
|
|
318
|
+
strictcli.App(
|
|
319
|
+
name="testapp", version="1.0.0", help="test app",
|
|
320
|
+
checks_path=str(toml_file),
|
|
321
|
+
checks_embed=VALID_TOML.encode(),
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
def test_checks_embed_invalid_toml(self, tmp_path, monkeypatch):
|
|
325
|
+
"""checks_embed with garbage bytes raises ValueError."""
|
|
326
|
+
monkeypatch.chdir(tmp_path)
|
|
327
|
+
|
|
328
|
+
with pytest.raises(ValueError, match="checks.toml:"):
|
|
329
|
+
strictcli.App(
|
|
330
|
+
name="testapp", version="1.0.0", help="test app",
|
|
331
|
+
checks_embed=b"\x80\x81 not valid toml {{{{",
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
def test_checks_embed_wrong_app_name(self, tmp_path, monkeypatch):
|
|
335
|
+
"""checks_embed with mismatched app name raises ValueError."""
|
|
336
|
+
monkeypatch.chdir(tmp_path)
|
|
337
|
+
wrong_toml = 'app = "wrong"\n'
|
|
338
|
+
|
|
339
|
+
with pytest.raises(
|
|
340
|
+
ValueError,
|
|
341
|
+
match=r'checks.toml: app "wrong" does not match app name "testapp"',
|
|
342
|
+
):
|
|
343
|
+
strictcli.App(
|
|
344
|
+
name="testapp", version="1.0.0", help="test app",
|
|
345
|
+
checks_embed=wrong_toml.encode(),
|
|
346
|
+
)
|