strictcli 0.10.0__tar.gz → 0.12.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.10.0/.rlsbl/bases → strictcli-0.12.0}/.github/workflows/ci.yml +1 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/.github/workflows/publish.yml +2 -0
- {strictcli-0.10.0 → strictcli-0.12.0/.rlsbl/bases}/.github/workflows/ci.yml +1 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/bases/.github/workflows/publish.yml +3 -1
- strictcli-0.12.0/.rlsbl/changes/.validated +1 -0
- strictcli-0.12.0/.rlsbl/changes/0.11.0.jsonl +7 -0
- strictcli-0.12.0/.rlsbl/changes/0.11.0.md +5 -0
- strictcli-0.12.0/.rlsbl/changes/0.12.0.jsonl +2 -0
- strictcli-0.12.0/.rlsbl/changes/0.12.0.md +5 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/config.json +2 -1
- {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/hashes.json +2 -2
- strictcli-0.12.0/.rlsbl/hooks/pre-checks.sh +10 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/hooks/pre-release.sh +5 -0
- strictcli-0.12.0/.rlsbl/releases/v0.12.0.toml +4 -0
- strictcli-0.12.0/.rlsbl/releases/v0.9.0.toml +3 -0
- strictcli-0.12.0/.rlsbl/version +1 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/CHANGELOG.md +12 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/PKG-INFO +1 -1
- {strictcli-0.10.0 → strictcli-0.12.0}/package-lock.json +2 -2
- {strictcli-0.10.0 → strictcli-0.12.0}/package.json +1 -1
- {strictcli-0.10.0 → strictcli-0.12.0}/pyproject.toml +1 -1
- {strictcli-0.10.0 → strictcli-0.12.0}/strictcli/__init__.py +28 -14
- {strictcli-0.10.0 → strictcli-0.12.0}/tests/test_check_command.py +17 -9
- strictcli-0.12.0/tests/test_check_discovery.py +295 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/tests/test_check_runner.py +7 -5
- {strictcli-0.10.0 → strictcli-0.12.0}/tests/test_check_schema.py +14 -6
- {strictcli-0.10.0 → strictcli-0.12.0}/tests/test_toml_loading.py +94 -5
- {strictcli-0.10.0 → strictcli-0.12.0}/uv.lock +1 -1
- strictcli-0.10.0/.rlsbl/changes/.validated +0 -1
- strictcli-0.10.0/.rlsbl/hooks/pre-checks.sh +0 -5
- strictcli-0.10.0/.rlsbl/version +0 -1
- strictcli-0.10.0/tests/test_check_discovery.py +0 -177
- {strictcli-0.10.0 → strictcli-0.12.0}/.claude/settings.json +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/.gitignore +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/bases/.claude/settings.json +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/bases/.gitignore +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/bases/.rlsbl/hooks/post-release.sh +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/bases/.rlsbl/hooks/pre-checks.sh +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/bases/.rlsbl/hooks/pre-release.sh +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/bases/.rlsbl/lint/go.toml +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/bases/.rlsbl/lint/npm.toml +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/bases/.rlsbl/lint/python.toml +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/bases/CHANGELOG.md +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/bases/CLAUDE.md +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/bases/LICENSE +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/changes/0.10.0.jsonl +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/changes/0.10.0.md +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/changes/0.4.0.jsonl +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/changes/0.4.0.md +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/changes/0.4.1.jsonl +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/changes/0.4.1.md +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/changes/0.5.0.jsonl +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/changes/0.5.0.md +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/changes/0.6.0.jsonl +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/changes/0.6.0.md +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/changes/0.6.1.jsonl +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/changes/0.6.1.md +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/changes/0.7.0.jsonl +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/changes/0.7.0.md +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/changes/0.7.1.jsonl +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/changes/0.7.1.md +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/changes/0.8.0.jsonl +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/changes/0.8.0.md +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/changes/0.8.1.jsonl +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/changes/0.8.1.md +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/changes/0.8.2.jsonl +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/changes/0.8.2.md +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/changes/0.8.3.jsonl +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/changes/0.8.3.md +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/changes/0.8.4.jsonl +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/changes/0.8.4.md +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/changes/0.8.5.jsonl +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/changes/0.8.5.md +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/changes/0.8.6.jsonl +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/changes/0.8.6.md +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/changes/0.8.7.jsonl +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/changes/0.8.7.md +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/changes/0.9.0.jsonl +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/changes/0.9.0.md +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/changes/0.9.1.jsonl +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/changes/0.9.1.md +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/changes/unreleased.jsonl +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/hooks/post-release.sh +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/lint/go.toml +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/lint/npm.toml +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/lint/python.toml +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/releases/unreleased.toml +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/releases/v0.10.0.toml +0 -0
- /strictcli-0.10.0/.rlsbl/releases/v0.9.0.toml → /strictcli-0.12.0/.rlsbl/releases/v0.11.0.toml +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/releases/v0.8.5.toml +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/releases/v0.8.6.toml +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/releases/v0.8.7.toml +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/releases/v0.9.1.toml +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/.strictcli/schema.json +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/CLAUDE.md +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/LICENSE +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/README.md +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/index.js +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/postinstall.js +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/tests/test_arg_default.py +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/tests/test_at_prefix.py +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/tests/test_auto_version.py +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/tests/test_check_types.py +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/tests/test_choices.py +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/tests/test_command_help_suggestion.py +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/tests/test_config.py +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/tests/test_config_file_path.py +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/tests/test_deep_nesting.py +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/tests/test_dependencies.py +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/tests/test_deprecated.py +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/tests/test_dump_schema.py +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/tests/test_e2e.py +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/tests/test_env.py +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/tests/test_exit_codes.py +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/tests/test_float_type.py +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/tests/test_global_flags.py +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/tests/test_help.py +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/tests/test_int_type.py +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/tests/test_mutex.py +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/tests/test_nesting.py +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/tests/test_parser.py +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/tests/test_passthrough.py +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/tests/test_registration.py +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/tests/test_repeatable.py +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/tests/test_tagdsl.py +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/tests/test_tags.py +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/tests/test_validate.py +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/tests/test_variadic.py +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/todo/.defer/deferred.md +0 -0
- {strictcli-0.10.0 → strictcli-0.12.0}/todo/.done/original-idea.md +0 -0
|
@@ -3,6 +3,8 @@ name: Publish
|
|
|
3
3
|
on:
|
|
4
4
|
release:
|
|
5
5
|
types: [published]
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
|
|
6
8
|
|
|
7
9
|
permissions:
|
|
8
10
|
contents: read
|
|
@@ -24,7 +26,7 @@ jobs:
|
|
|
24
26
|
- uses: actions/setup-node@v6
|
|
25
27
|
with:
|
|
26
28
|
node-version: 24
|
|
27
|
-
registry-url:
|
|
29
|
+
registry-url: https://registry.npmjs.org
|
|
28
30
|
- run: npm publish --provenance --access public
|
|
29
31
|
env:
|
|
30
32
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
c161283293520cc94f39d9cfec589f6f4cf5a352
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{"commits":["feae73fae6f1e08f1355bc7c1b9beae842397517"],"user_facing":false}
|
|
2
|
+
{"commits":["087bd6cb358bf6ebf99e2635e93a2fdc59ac5517"],"user_facing":false}
|
|
3
|
+
{"commits":["f7e07e1d5ce69895df1a95da9c51ab686300e0b3"],"user_facing":false}
|
|
4
|
+
{"commits":["f96d157204a50c577f78b99667be81472e23bfe7"],"user_facing":false}
|
|
5
|
+
{"commits":["c7ee8c1401de38d9e086c38284388497c54cca0a"],"user_facing":true,"description":"**New feature.** `checks_path` parameter on `App` for explicit checks.toml location, replacing CWD-based discovery.","type":"feature"}
|
|
6
|
+
{"commits":["dd7a103769db5bccfaabb2636fce0ef5082e2159","df35fbde862721ef8b01a483abf6b33e5387afa8"],"user_facing":false}
|
|
7
|
+
{"commits":["f94f50bad6401756ca75ee82b31fbfc7e85edc83"],"user_facing":false}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
{"commits":["495cf93ada5d5870e1fa83594b7c856f2f493f19","d604e97d59fa0e252bfd179a2e4d0f0f62757677"],"user_facing":true,"description":"**Breaking.** CWD auto-discovery of checks.toml removed; checks must be explicitly enabled via `checks_path=`. checks.toml now requires a top-level `app` field matching the app name.","type":"breaking"}
|
|
2
|
+
{"commits":["fb4899a3e17880d31ac56b42aca24d359528c8af"],"user_facing":false}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
".github/workflows/ci.yml": "
|
|
3
|
-
".github/workflows/publish.yml": "
|
|
2
|
+
".github/workflows/ci.yml": "b4e075171b131d25f9cce55a3e3a30732e95a2a794af00f354ee3200626d6a04",
|
|
3
|
+
".github/workflows/publish.yml": "8befe0164321eb68e9465c634aa15d6ccb30d46fa669f7d30bf3ed064ad258ba",
|
|
4
4
|
"CHANGELOG.md": "7a6ec46141007c18090a0ff325e2dd68cd51588c4e0b5af7b580365e5eaaca85",
|
|
5
5
|
".gitignore": "c89d76c6c43e7f0897b3472a15b7c0103e0be5b3a04c2b44e585fa48bdcda2bb",
|
|
6
6
|
"LICENSE": "e958892abc1dd5d991fca6d528e695d335f25773d373d2e0be5f4a9507d8f8d5",
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
# This hook runs BEFORE built-in pre-release checks (tests, lint).
|
|
4
|
+
# Use it for setup tasks: starting services, setting env vars, etc.
|
|
5
|
+
# Built-in checks run after this hook. Custom validation goes in pre-release.sh.
|
|
6
|
+
|
|
7
|
+
# Ensure strictcli and conformance tool are installed (editable)
|
|
8
|
+
REPO_ROOT="$(git rev-parse --show-toplevel)"
|
|
9
|
+
pip install -e "$REPO_ROOT/python" --break-system-packages --quiet 2>/dev/null || true
|
|
10
|
+
pip install -e "$REPO_ROOT/conformance" --break-system-packages --quiet 2>/dev/null || true
|
|
@@ -6,3 +6,8 @@ set -euo pipefail
|
|
|
6
6
|
# - Check for uncommitted documentation
|
|
7
7
|
# - Verify external service connectivity
|
|
8
8
|
# - Run integration tests not covered by the test suite
|
|
9
|
+
|
|
10
|
+
# Run conformance checks before release
|
|
11
|
+
REPO_ROOT="$(git rev-parse --show-toplevel)"
|
|
12
|
+
cd "$REPO_ROOT/conformance"
|
|
13
|
+
conformance check --tag pre-release
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.45.0
|
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
# Changelog
|
|
4
4
|
|
|
5
|
+
## 0.12.0
|
|
6
|
+
|
|
7
|
+
### Breaking
|
|
8
|
+
|
|
9
|
+
- **Breaking.** CWD auto-discovery of checks.toml removed; checks must be explicitly enabled via `checks_path=`. checks.toml now requires a top-level `app` field matching the app name.
|
|
10
|
+
|
|
11
|
+
## 0.11.0
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
- **New feature.** `checks_path` parameter on `App` for explicit checks.toml location, replacing CWD-based discovery.
|
|
16
|
+
|
|
5
17
|
## 0.10.0
|
|
6
18
|
|
|
7
19
|
### Features
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "strictcli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "strictcli",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.12.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.12.0"
|
|
6
6
|
|
|
7
7
|
__all__ = [
|
|
8
8
|
"App", "Flag", "Arg", "Tag", "MutexGroup", "CoRequired", "Requires",
|
|
@@ -130,8 +130,8 @@ _CHECK_REQUIRED_FIELDS = {"tags", "severity", "fast", "pure", "needs_network", "
|
|
|
130
130
|
_CHECK_VALID_SEVERITIES = {"error", "warn"}
|
|
131
131
|
|
|
132
132
|
|
|
133
|
-
def _load_checks_toml(path: str | Path) -> dict[str, _CheckDef]:
|
|
134
|
-
"""Parse and validate a checks.toml file, returning
|
|
133
|
+
def _load_checks_toml(path: str | Path) -> tuple[str, dict[str, _CheckDef]]:
|
|
134
|
+
"""Parse and validate a checks.toml file, returning (app_name, check_defs).
|
|
135
135
|
|
|
136
136
|
Raises ValueError on any schema violation, file error, or invalid TOML.
|
|
137
137
|
"""
|
|
@@ -145,13 +145,20 @@ def _load_checks_toml(path: str | Path) -> dict[str, _CheckDef]:
|
|
|
145
145
|
except (tomllib.TOMLDecodeError, UnicodeDecodeError) as exc:
|
|
146
146
|
raise ValueError(f"checks.toml: {exc}") from exc
|
|
147
147
|
|
|
148
|
-
# Only [checks]
|
|
148
|
+
# Only "app" and [checks] are allowed at the top level
|
|
149
149
|
for key in data:
|
|
150
|
-
if key
|
|
150
|
+
if key not in ("app", "checks"):
|
|
151
151
|
raise ValueError(f'checks.toml: unknown top-level key "{key}"')
|
|
152
152
|
|
|
153
|
+
# Validate required "app" field
|
|
154
|
+
if "app" not in data:
|
|
155
|
+
raise ValueError('checks.toml: missing required top-level key "app"')
|
|
156
|
+
if not isinstance(data["app"], str) or not data["app"]:
|
|
157
|
+
raise ValueError('checks.toml: "app" must be a non-empty string')
|
|
158
|
+
app_name = data["app"]
|
|
159
|
+
|
|
153
160
|
if "checks" not in data:
|
|
154
|
-
return {}
|
|
161
|
+
return (app_name, {})
|
|
155
162
|
|
|
156
163
|
checks_section = data["checks"]
|
|
157
164
|
if not isinstance(checks_section, dict):
|
|
@@ -243,7 +250,7 @@ def _load_checks_toml(path: str | Path) -> dict[str, _CheckDef]:
|
|
|
243
250
|
f'unknown check "{dep}"'
|
|
244
251
|
)
|
|
245
252
|
|
|
246
|
-
return result
|
|
253
|
+
return (app_name, result)
|
|
247
254
|
|
|
248
255
|
|
|
249
256
|
class _HelpRequested(Exception):
|
|
@@ -667,6 +674,7 @@ class App:
|
|
|
667
674
|
config: bool = False
|
|
668
675
|
config_path: str | None = None
|
|
669
676
|
config_format: str = "json"
|
|
677
|
+
checks_path: str | Path | None = None
|
|
670
678
|
flags: list[Flag] = field(default_factory=list)
|
|
671
679
|
_commands: dict[str, Command] = field(default_factory=dict)
|
|
672
680
|
_groups: dict[str, Group] = field(default_factory=dict)
|
|
@@ -704,13 +712,19 @@ class App:
|
|
|
704
712
|
self._register_config_group()
|
|
705
713
|
# Discover checks TOML
|
|
706
714
|
self._check_context_factory: Callable | None = None
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
715
|
+
if self.checks_path is not None:
|
|
716
|
+
checks_toml_path = Path(self.checks_path).resolve()
|
|
717
|
+
if not checks_toml_path.is_file():
|
|
718
|
+
raise ValueError(f"checks_path does not exist: {self.checks_path}")
|
|
719
|
+
app_name, self._check_defs = _load_checks_toml(checks_toml_path)
|
|
720
|
+
if app_name != self.name:
|
|
721
|
+
raise ValueError(
|
|
722
|
+
f'checks.toml: app "{app_name}" does not match app name "{self.name}"'
|
|
723
|
+
)
|
|
710
724
|
self._checks_enabled = True
|
|
711
725
|
self._register_check_command()
|
|
712
726
|
else:
|
|
713
|
-
self._check_defs = {}
|
|
727
|
+
self._check_defs: dict[str, _CheckDef] = {}
|
|
714
728
|
self._checks_enabled = False
|
|
715
729
|
|
|
716
730
|
@property
|
|
@@ -724,12 +738,12 @@ class App:
|
|
|
724
738
|
if not self._checks_enabled:
|
|
725
739
|
raise ValueError(
|
|
726
740
|
f'cannot register check "{name}": '
|
|
727
|
-
f"
|
|
741
|
+
f"checks not enabled"
|
|
728
742
|
)
|
|
729
743
|
if name not in self._check_defs:
|
|
730
744
|
raise ValueError(
|
|
731
745
|
f'cannot register check "{name}": '
|
|
732
|
-
f"not declared in
|
|
746
|
+
f"not declared in checks.toml"
|
|
733
747
|
)
|
|
734
748
|
if self._check_defs[name].impl is not None:
|
|
735
749
|
raise ValueError(f'check "{name}": duplicate registration')
|
|
@@ -750,7 +764,7 @@ class App:
|
|
|
750
764
|
)
|
|
751
765
|
if missing:
|
|
752
766
|
return (
|
|
753
|
-
"checks declared in
|
|
767
|
+
"checks declared in checks.toml but not registered: "
|
|
754
768
|
+ ", ".join(missing)
|
|
755
769
|
)
|
|
756
770
|
return None
|
|
@@ -11,6 +11,8 @@ from strictcli import CheckResult
|
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
TWO_CHECKS_TOML = """\
|
|
14
|
+
app = "testapp"
|
|
15
|
+
|
|
14
16
|
[checks.version-check]
|
|
15
17
|
tags = ["release"]
|
|
16
18
|
severity = "error"
|
|
@@ -29,6 +31,8 @@ depends_on = []
|
|
|
29
31
|
"""
|
|
30
32
|
|
|
31
33
|
THREE_CHECKS_WITH_DEP_TOML = """\
|
|
34
|
+
app = "testapp"
|
|
35
|
+
|
|
32
36
|
[checks.base-check]
|
|
33
37
|
tags = ["infra"]
|
|
34
38
|
severity = "error"
|
|
@@ -66,11 +70,13 @@ def _setup_checks_app(tmp_path, monkeypatch, toml_content, register_impls=True,
|
|
|
66
70
|
|
|
67
71
|
pass_results: dict mapping check name to CheckResult. If None, all return pass.
|
|
68
72
|
"""
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
monkeypatch.chdir(tmp_path)
|
|
73
|
+
toml_file = tmp_path / "checks.toml"
|
|
74
|
+
toml_file.write_text(toml_content)
|
|
72
75
|
|
|
73
|
-
app = strictcli.App(
|
|
76
|
+
app = strictcli.App(
|
|
77
|
+
name="testapp", version="1.0.0", help="test app",
|
|
78
|
+
checks_path=str(toml_file),
|
|
79
|
+
)
|
|
74
80
|
|
|
75
81
|
if register_impls:
|
|
76
82
|
if pass_results is None:
|
|
@@ -317,13 +323,15 @@ class TestCheckIgnoreWarnings:
|
|
|
317
323
|
|
|
318
324
|
|
|
319
325
|
class TestCheckNoContextFactory:
|
|
320
|
-
def test_no_context_factory_error(self, tmp_path
|
|
326
|
+
def test_no_context_factory_error(self, tmp_path):
|
|
321
327
|
"""Running checks without set_check_context produces error."""
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
monkeypatch.chdir(tmp_path)
|
|
328
|
+
toml_file = tmp_path / "checks.toml"
|
|
329
|
+
toml_file.write_text(TWO_CHECKS_TOML)
|
|
325
330
|
|
|
326
|
-
app = strictcli.App(
|
|
331
|
+
app = strictcli.App(
|
|
332
|
+
name="testapp", version="1.0.0", help="test app",
|
|
333
|
+
checks_path=str(toml_file),
|
|
334
|
+
)
|
|
327
335
|
for name in app._check_defs:
|
|
328
336
|
app._check_defs[name].impl = (
|
|
329
337
|
lambda ctx, n=name: CheckResult(status="pass", message=f"{n} OK")
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
"""Tests for check discovery and double-entry registration (Phase 3)."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
import strictcli
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
VALID_TOML = """\
|
|
9
|
+
app = "testapp"
|
|
10
|
+
|
|
11
|
+
[checks.lint-code]
|
|
12
|
+
tags = ["code", "fast"]
|
|
13
|
+
severity = "error"
|
|
14
|
+
fast = true
|
|
15
|
+
pure = true
|
|
16
|
+
needs_network = false
|
|
17
|
+
depends_on = []
|
|
18
|
+
|
|
19
|
+
[checks.check-deps]
|
|
20
|
+
tags = ["deps"]
|
|
21
|
+
severity = "warn"
|
|
22
|
+
fast = false
|
|
23
|
+
pure = false
|
|
24
|
+
needs_network = true
|
|
25
|
+
depends_on = ["lint-code"]
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
INVALID_TOML = """\
|
|
29
|
+
app = "testapp"
|
|
30
|
+
|
|
31
|
+
[checks.BadName]
|
|
32
|
+
tags = ["a"]
|
|
33
|
+
severity = "error"
|
|
34
|
+
fast = true
|
|
35
|
+
pure = true
|
|
36
|
+
needs_network = false
|
|
37
|
+
depends_on = []
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class TestDiscovery:
|
|
42
|
+
def test_no_checks_path_ignores_cwd(self, tmp_path, monkeypatch):
|
|
43
|
+
"""Without checks_path, CWD checks.toml is NOT discovered."""
|
|
44
|
+
(tmp_path / ".strictcli").mkdir()
|
|
45
|
+
(tmp_path / ".strictcli" / "checks.toml").write_text(VALID_TOML)
|
|
46
|
+
monkeypatch.chdir(tmp_path)
|
|
47
|
+
|
|
48
|
+
app = strictcli.App(name="testapp", version="1.0.0", help="test app")
|
|
49
|
+
assert app._checks_enabled is False
|
|
50
|
+
assert app._check_defs == {}
|
|
51
|
+
|
|
52
|
+
def test_no_toml_sets_checks_disabled(self, tmp_path, monkeypatch):
|
|
53
|
+
monkeypatch.chdir(tmp_path)
|
|
54
|
+
|
|
55
|
+
app = strictcli.App(name="testapp", version="1.0.0", help="test app")
|
|
56
|
+
assert app._checks_enabled is False
|
|
57
|
+
assert app._check_defs == {}
|
|
58
|
+
|
|
59
|
+
def test_invalid_toml_raises_valueerror(self, tmp_path):
|
|
60
|
+
toml_file = tmp_path / "checks.toml"
|
|
61
|
+
toml_file.write_text(INVALID_TOML)
|
|
62
|
+
|
|
63
|
+
with pytest.raises(ValueError, match='invalid check name "BadName"'):
|
|
64
|
+
strictcli.App(
|
|
65
|
+
name="testapp", version="1.0.0", help="test app",
|
|
66
|
+
checks_path=str(toml_file),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class TestChecksPath:
|
|
71
|
+
def test_explicit_path_discovers_checks(self, tmp_path, monkeypatch):
|
|
72
|
+
"""checks_path points to a valid TOML in a non-CWD directory."""
|
|
73
|
+
# Put checks.toml somewhere that is NOT CWD/.strictcli/
|
|
74
|
+
custom_dir = tmp_path / "custom"
|
|
75
|
+
custom_dir.mkdir()
|
|
76
|
+
toml_file = custom_dir / "checks.toml"
|
|
77
|
+
toml_file.write_text(VALID_TOML)
|
|
78
|
+
|
|
79
|
+
# chdir to a directory with NO .strictcli/
|
|
80
|
+
empty_dir = tmp_path / "empty"
|
|
81
|
+
empty_dir.mkdir()
|
|
82
|
+
monkeypatch.chdir(empty_dir)
|
|
83
|
+
|
|
84
|
+
app = strictcli.App(
|
|
85
|
+
name="testapp", version="1.0.0", help="test app",
|
|
86
|
+
checks_path=str(toml_file),
|
|
87
|
+
)
|
|
88
|
+
assert app._checks_enabled is True
|
|
89
|
+
assert "lint-code" in app._check_defs
|
|
90
|
+
assert "check-deps" in app._check_defs
|
|
91
|
+
|
|
92
|
+
def test_explicit_path_as_pathlib(self, tmp_path, monkeypatch):
|
|
93
|
+
"""checks_path accepts a Path object."""
|
|
94
|
+
custom_dir = tmp_path / "custom"
|
|
95
|
+
custom_dir.mkdir()
|
|
96
|
+
toml_file = custom_dir / "checks.toml"
|
|
97
|
+
toml_file.write_text(VALID_TOML)
|
|
98
|
+
|
|
99
|
+
empty_dir = tmp_path / "empty"
|
|
100
|
+
empty_dir.mkdir()
|
|
101
|
+
monkeypatch.chdir(empty_dir)
|
|
102
|
+
|
|
103
|
+
from pathlib import Path
|
|
104
|
+
|
|
105
|
+
app = strictcli.App(
|
|
106
|
+
name="testapp", version="1.0.0", help="test app",
|
|
107
|
+
checks_path=Path(str(toml_file)),
|
|
108
|
+
)
|
|
109
|
+
assert app._checks_enabled is True
|
|
110
|
+
assert "lint-code" in app._check_defs
|
|
111
|
+
|
|
112
|
+
def test_nonexistent_path_raises(self, tmp_path, monkeypatch):
|
|
113
|
+
"""checks_path pointing to a missing file raises ValueError."""
|
|
114
|
+
monkeypatch.chdir(tmp_path)
|
|
115
|
+
bad_path = tmp_path / "nope" / "checks.toml"
|
|
116
|
+
|
|
117
|
+
with pytest.raises(ValueError, match="checks_path does not exist"):
|
|
118
|
+
strictcli.App(
|
|
119
|
+
name="testapp", version="1.0.0", help="test app",
|
|
120
|
+
checks_path=str(bad_path),
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
def test_none_means_disabled(self, tmp_path, monkeypatch):
|
|
124
|
+
"""checks_path=None (default) means checks are disabled."""
|
|
125
|
+
(tmp_path / ".strictcli").mkdir()
|
|
126
|
+
(tmp_path / ".strictcli" / "checks.toml").write_text(VALID_TOML)
|
|
127
|
+
monkeypatch.chdir(tmp_path)
|
|
128
|
+
|
|
129
|
+
app = strictcli.App(
|
|
130
|
+
name="testapp", version="1.0.0", help="test app",
|
|
131
|
+
checks_path=None,
|
|
132
|
+
)
|
|
133
|
+
assert app._checks_enabled is False
|
|
134
|
+
assert app._check_defs == {}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class TestCheckDecorator:
|
|
138
|
+
def test_registers_implementation(self, tmp_path):
|
|
139
|
+
toml_file = tmp_path / "checks.toml"
|
|
140
|
+
toml_file.write_text(VALID_TOML)
|
|
141
|
+
|
|
142
|
+
app = strictcli.App(
|
|
143
|
+
name="testapp", version="1.0.0", help="test app",
|
|
144
|
+
checks_path=str(toml_file),
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
@app.check("lint-code")
|
|
148
|
+
def lint_impl(ctx):
|
|
149
|
+
return strictcli.CheckResult(status="pass", message="OK")
|
|
150
|
+
|
|
151
|
+
assert app._check_defs["lint-code"].impl is lint_impl
|
|
152
|
+
|
|
153
|
+
def test_undeclared_name_raises(self, tmp_path):
|
|
154
|
+
toml_file = tmp_path / "checks.toml"
|
|
155
|
+
toml_file.write_text(VALID_TOML)
|
|
156
|
+
|
|
157
|
+
app = strictcli.App(
|
|
158
|
+
name="testapp", version="1.0.0", help="test app",
|
|
159
|
+
checks_path=str(toml_file),
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
with pytest.raises(ValueError, match='cannot register check "nonexistent"'):
|
|
163
|
+
@app.check("nonexistent")
|
|
164
|
+
def bad_impl(ctx):
|
|
165
|
+
pass
|
|
166
|
+
|
|
167
|
+
def test_duplicate_registration_raises(self, tmp_path):
|
|
168
|
+
toml_file = tmp_path / "checks.toml"
|
|
169
|
+
toml_file.write_text(VALID_TOML)
|
|
170
|
+
|
|
171
|
+
app = strictcli.App(
|
|
172
|
+
name="testapp", version="1.0.0", help="test app",
|
|
173
|
+
checks_path=str(toml_file),
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
@app.check("lint-code")
|
|
177
|
+
def first(ctx):
|
|
178
|
+
pass
|
|
179
|
+
|
|
180
|
+
with pytest.raises(ValueError, match='check "lint-code": duplicate registration'):
|
|
181
|
+
@app.check("lint-code")
|
|
182
|
+
def second(ctx):
|
|
183
|
+
pass
|
|
184
|
+
|
|
185
|
+
def test_no_toml_raises(self, tmp_path, monkeypatch):
|
|
186
|
+
monkeypatch.chdir(tmp_path)
|
|
187
|
+
|
|
188
|
+
app = strictcli.App(name="testapp", version="1.0.0", help="test app")
|
|
189
|
+
|
|
190
|
+
with pytest.raises(ValueError, match="checks not enabled"):
|
|
191
|
+
@app.check("anything")
|
|
192
|
+
def bad(ctx):
|
|
193
|
+
pass
|
|
194
|
+
|
|
195
|
+
def test_register_check_not_enabled_message(self, tmp_path):
|
|
196
|
+
"""Verify exact error message for registering check when not enabled."""
|
|
197
|
+
app = strictcli.App(name="testapp", version="1.0.0", help="test app")
|
|
198
|
+
|
|
199
|
+
with pytest.raises(
|
|
200
|
+
ValueError,
|
|
201
|
+
match=r'cannot register check "my-check": checks not enabled',
|
|
202
|
+
):
|
|
203
|
+
@app.check("my-check")
|
|
204
|
+
def impl(ctx):
|
|
205
|
+
pass
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
class TestDoubleEntryValidation:
|
|
209
|
+
def test_missing_impl_error_in_test(self, tmp_path):
|
|
210
|
+
toml_file = tmp_path / "checks.toml"
|
|
211
|
+
toml_file.write_text(VALID_TOML)
|
|
212
|
+
|
|
213
|
+
app = strictcli.App(
|
|
214
|
+
name="testapp", version="1.0.0", help="test app",
|
|
215
|
+
checks_path=str(toml_file),
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
# Register a dummy command so the app has something to run
|
|
219
|
+
@app.command("hello", help="say hello")
|
|
220
|
+
def hello(**kw):
|
|
221
|
+
print("hello")
|
|
222
|
+
|
|
223
|
+
# Only register one of the two checks
|
|
224
|
+
@app.check("lint-code")
|
|
225
|
+
def lint_impl(ctx):
|
|
226
|
+
return strictcli.CheckResult(status="pass", message="OK")
|
|
227
|
+
|
|
228
|
+
# check-deps is not registered -- should fail validation
|
|
229
|
+
result = app.test(["hello"])
|
|
230
|
+
assert result.exit_code == 1
|
|
231
|
+
assert "checks declared in checks.toml but not registered" in result.stderr
|
|
232
|
+
assert "check-deps" in result.stderr
|
|
233
|
+
|
|
234
|
+
def test_all_registered_passes_validation(self, tmp_path):
|
|
235
|
+
toml_file = tmp_path / "checks.toml"
|
|
236
|
+
toml_file.write_text(VALID_TOML)
|
|
237
|
+
|
|
238
|
+
app = strictcli.App(
|
|
239
|
+
name="testapp", version="1.0.0", help="test app",
|
|
240
|
+
checks_path=str(toml_file),
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
@app.command("hello", help="say hello")
|
|
244
|
+
def hello(**kw):
|
|
245
|
+
print("hello")
|
|
246
|
+
|
|
247
|
+
@app.check("lint-code")
|
|
248
|
+
def lint_impl(ctx):
|
|
249
|
+
return strictcli.CheckResult(status="pass", message="OK")
|
|
250
|
+
|
|
251
|
+
@app.check("check-deps")
|
|
252
|
+
def deps_impl(ctx):
|
|
253
|
+
return strictcli.CheckResult(status="pass", message="OK")
|
|
254
|
+
|
|
255
|
+
result = app.test(["hello"])
|
|
256
|
+
assert result.exit_code == 0
|
|
257
|
+
assert "hello" in result.stdout
|
|
258
|
+
|
|
259
|
+
def test_no_checks_skips_validation(self, tmp_path, monkeypatch):
|
|
260
|
+
monkeypatch.chdir(tmp_path)
|
|
261
|
+
|
|
262
|
+
app = strictcli.App(name="testapp", version="1.0.0", help="test app")
|
|
263
|
+
|
|
264
|
+
@app.command("hello", help="say hello")
|
|
265
|
+
def hello(**kw):
|
|
266
|
+
print("hello")
|
|
267
|
+
|
|
268
|
+
result = app.test(["hello"])
|
|
269
|
+
assert result.exit_code == 0
|
|
270
|
+
assert "hello" in result.stdout
|
|
271
|
+
|
|
272
|
+
def test_app_mismatch_raises(self, tmp_path):
|
|
273
|
+
"""checks_path with app='wrong' but App name='testapp' raises ValueError."""
|
|
274
|
+
toml = """\
|
|
275
|
+
app = "wrong"
|
|
276
|
+
|
|
277
|
+
[checks.my-check]
|
|
278
|
+
tags = ["a"]
|
|
279
|
+
severity = "error"
|
|
280
|
+
fast = true
|
|
281
|
+
pure = true
|
|
282
|
+
needs_network = false
|
|
283
|
+
depends_on = []
|
|
284
|
+
"""
|
|
285
|
+
toml_file = tmp_path / "checks.toml"
|
|
286
|
+
toml_file.write_text(toml)
|
|
287
|
+
|
|
288
|
+
with pytest.raises(
|
|
289
|
+
ValueError,
|
|
290
|
+
match=r'checks.toml: app "wrong" does not match app name "testapp"',
|
|
291
|
+
):
|
|
292
|
+
strictcli.App(
|
|
293
|
+
name="testapp", version="1.0.0", help="test app",
|
|
294
|
+
checks_path=str(toml_file),
|
|
295
|
+
)
|