strictcli 0.11.0__tar.gz → 0.13.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.13.0/.rlsbl/changes/.validated +1 -0
- strictcli-0.13.0/.rlsbl/changes/0.12.0.jsonl +2 -0
- strictcli-0.13.0/.rlsbl/changes/0.12.0.md +5 -0
- strictcli-0.13.0/.rlsbl/changes/0.13.0.jsonl +3 -0
- strictcli-0.13.0/.rlsbl/releases/v0.12.0.toml +4 -0
- strictcli-0.13.0/.rlsbl/releases/v0.13.0.toml +4 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/CHANGELOG.md +12 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/PKG-INFO +1 -1
- {strictcli-0.11.0 → strictcli-0.13.0}/package-lock.json +2 -2
- {strictcli-0.11.0 → strictcli-0.13.0}/package.json +1 -1
- {strictcli-0.11.0 → strictcli-0.13.0}/pyproject.toml +1 -1
- {strictcli-0.11.0 → strictcli-0.13.0}/strictcli/__init__.py +56 -18
- {strictcli-0.11.0 → strictcli-0.13.0}/tests/test_check_command.py +17 -9
- {strictcli-0.11.0 → strictcli-0.13.0}/tests/test_check_discovery.py +93 -42
- {strictcli-0.11.0 → strictcli-0.13.0}/tests/test_check_runner.py +7 -5
- {strictcli-0.11.0 → strictcli-0.13.0}/tests/test_check_schema.py +21 -6
- {strictcli-0.11.0 → strictcli-0.13.0}/tests/test_dump_schema.py +66 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/tests/test_toml_loading.py +94 -5
- {strictcli-0.11.0 → strictcli-0.13.0}/uv.lock +1 -1
- strictcli-0.11.0/.rlsbl/changes/.validated +0 -1
- {strictcli-0.11.0 → strictcli-0.13.0}/.claude/settings.json +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/.github/workflows/ci.yml +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/.github/workflows/publish.yml +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/.gitignore +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/bases/.claude/settings.json +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/bases/.github/workflows/ci.yml +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/bases/.github/workflows/publish.yml +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/bases/.gitignore +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/bases/.rlsbl/hooks/post-release.sh +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/bases/.rlsbl/hooks/pre-checks.sh +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/bases/.rlsbl/hooks/pre-release.sh +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/bases/.rlsbl/lint/go.toml +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/bases/.rlsbl/lint/npm.toml +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/bases/.rlsbl/lint/python.toml +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/bases/CHANGELOG.md +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/bases/CLAUDE.md +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/bases/LICENSE +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/changes/0.10.0.jsonl +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/changes/0.10.0.md +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/changes/0.11.0.jsonl +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/changes/0.11.0.md +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/changes/0.4.0.jsonl +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/changes/0.4.0.md +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/changes/0.4.1.jsonl +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/changes/0.4.1.md +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/changes/0.5.0.jsonl +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/changes/0.5.0.md +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/changes/0.6.0.jsonl +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/changes/0.6.0.md +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/changes/0.6.1.jsonl +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/changes/0.6.1.md +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/changes/0.7.0.jsonl +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/changes/0.7.0.md +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/changes/0.7.1.jsonl +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/changes/0.7.1.md +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/changes/0.8.0.jsonl +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/changes/0.8.0.md +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/changes/0.8.1.jsonl +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/changes/0.8.1.md +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/changes/0.8.2.jsonl +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/changes/0.8.2.md +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/changes/0.8.3.jsonl +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/changes/0.8.3.md +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/changes/0.8.4.jsonl +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/changes/0.8.4.md +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/changes/0.8.5.jsonl +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/changes/0.8.5.md +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/changes/0.8.6.jsonl +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/changes/0.8.6.md +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/changes/0.8.7.jsonl +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/changes/0.8.7.md +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/changes/0.9.0.jsonl +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/changes/0.9.0.md +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/changes/0.9.1.jsonl +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/changes/0.9.1.md +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/changes/unreleased.jsonl +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/config.json +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/hashes.json +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/hooks/post-release.sh +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/hooks/pre-checks.sh +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/hooks/pre-release.sh +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/lint/go.toml +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/lint/npm.toml +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/lint/python.toml +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/releases/unreleased.toml +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/releases/v0.10.0.toml +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/releases/v0.11.0.toml +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/releases/v0.8.5.toml +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/releases/v0.8.6.toml +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/releases/v0.8.7.toml +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/releases/v0.9.0.toml +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/releases/v0.9.1.toml +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/version +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/.strictcli/schema.json +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/CLAUDE.md +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/LICENSE +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/README.md +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/index.js +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/postinstall.js +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/tests/test_arg_default.py +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/tests/test_at_prefix.py +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/tests/test_auto_version.py +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/tests/test_check_types.py +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/tests/test_choices.py +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/tests/test_command_help_suggestion.py +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/tests/test_config.py +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/tests/test_config_file_path.py +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/tests/test_deep_nesting.py +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/tests/test_dependencies.py +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/tests/test_deprecated.py +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/tests/test_e2e.py +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/tests/test_env.py +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/tests/test_exit_codes.py +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/tests/test_float_type.py +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/tests/test_global_flags.py +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/tests/test_help.py +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/tests/test_int_type.py +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/tests/test_mutex.py +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/tests/test_nesting.py +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/tests/test_parser.py +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/tests/test_passthrough.py +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/tests/test_registration.py +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/tests/test_repeatable.py +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/tests/test_tagdsl.py +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/tests/test_tags.py +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/tests/test_validate.py +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/tests/test_variadic.py +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/todo/.defer/deferred.md +0 -0
- {strictcli-0.11.0 → strictcli-0.13.0}/todo/.done/original-idea.md +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
052122abb24b7318d56b1c0d657fbaf740d6f5a5
|
|
@@ -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}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
{"commits":["12ad7c022938b07cf1f44b20bdbe9de0ae6a84be"],"user_facing":true,"description":"**New feature.** Schema dump (--dump-schema) now includes a project_id field read from pyproject.toml, providing provenance for schema validation.","type":"feature"}
|
|
2
|
+
{"commits":["a50cf7899c938974b12e8e74201bdf58c0205a55"],"user_facing":false}
|
|
3
|
+
{"commits":["214721745a82a47189a13b2443fb9ddc59826347"],"user_facing":false}
|
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
# Changelog
|
|
4
4
|
|
|
5
|
+
## 0.13.0
|
|
6
|
+
|
|
7
|
+
### Features
|
|
8
|
+
|
|
9
|
+
- **New feature.** Schema dump (--dump-schema) now includes a project_id field read from pyproject.toml, providing provenance for schema validation.
|
|
10
|
+
|
|
11
|
+
## 0.12.0
|
|
12
|
+
|
|
13
|
+
### Breaking
|
|
14
|
+
|
|
15
|
+
- **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.
|
|
16
|
+
|
|
5
17
|
## 0.11.0
|
|
6
18
|
|
|
7
19
|
### Features
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "strictcli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "strictcli",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.13.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.13.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):
|
|
@@ -709,14 +716,15 @@ class App:
|
|
|
709
716
|
checks_toml_path = Path(self.checks_path).resolve()
|
|
710
717
|
if not checks_toml_path.is_file():
|
|
711
718
|
raise ValueError(f"checks_path does not exist: {self.checks_path}")
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
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
|
+
)
|
|
716
724
|
self._checks_enabled = True
|
|
717
725
|
self._register_check_command()
|
|
718
726
|
else:
|
|
719
|
-
self._check_defs = {}
|
|
727
|
+
self._check_defs: dict[str, _CheckDef] = {}
|
|
720
728
|
self._checks_enabled = False
|
|
721
729
|
|
|
722
730
|
@property
|
|
@@ -730,12 +738,12 @@ class App:
|
|
|
730
738
|
if not self._checks_enabled:
|
|
731
739
|
raise ValueError(
|
|
732
740
|
f'cannot register check "{name}": '
|
|
733
|
-
f"
|
|
741
|
+
f"checks not enabled"
|
|
734
742
|
)
|
|
735
743
|
if name not in self._check_defs:
|
|
736
744
|
raise ValueError(
|
|
737
745
|
f'cannot register check "{name}": '
|
|
738
|
-
f"not declared in
|
|
746
|
+
f"not declared in checks.toml"
|
|
739
747
|
)
|
|
740
748
|
if self._check_defs[name].impl is not None:
|
|
741
749
|
raise ValueError(f'check "{name}": duplicate registration')
|
|
@@ -756,7 +764,7 @@ class App:
|
|
|
756
764
|
)
|
|
757
765
|
if missing:
|
|
758
766
|
return (
|
|
759
|
-
"checks declared in
|
|
767
|
+
"checks declared in checks.toml but not registered: "
|
|
760
768
|
+ ", ".join(missing)
|
|
761
769
|
)
|
|
762
770
|
return None
|
|
@@ -1452,7 +1460,11 @@ class App:
|
|
|
1452
1460
|
print(_format_version(self))
|
|
1453
1461
|
sys.exit(0)
|
|
1454
1462
|
except _DumpSchemaRequested:
|
|
1455
|
-
|
|
1463
|
+
try:
|
|
1464
|
+
path = _write_schema(self)
|
|
1465
|
+
except RuntimeError as e:
|
|
1466
|
+
print(f"error: {e}", file=sys.stderr)
|
|
1467
|
+
sys.exit(1)
|
|
1456
1468
|
print(path)
|
|
1457
1469
|
sys.exit(0)
|
|
1458
1470
|
except _ParseError as e:
|
|
@@ -1495,8 +1507,13 @@ class App:
|
|
|
1495
1507
|
except _VersionRequested:
|
|
1496
1508
|
stdout_buf.write(_format_version(self) + "\n")
|
|
1497
1509
|
except _DumpSchemaRequested:
|
|
1498
|
-
|
|
1499
|
-
|
|
1510
|
+
try:
|
|
1511
|
+
path = _write_schema(self)
|
|
1512
|
+
except RuntimeError as e:
|
|
1513
|
+
stderr_buf.write(f"error: {e}\n")
|
|
1514
|
+
exit_code = 1
|
|
1515
|
+
else:
|
|
1516
|
+
stdout_buf.write(path + "\n")
|
|
1500
1517
|
except _ParseError as e:
|
|
1501
1518
|
stderr_buf.write(f"error: {e}\n")
|
|
1502
1519
|
prefix = e.command_prefix or self.name
|
|
@@ -3079,14 +3096,35 @@ def _build_schema_defaults() -> dict:
|
|
|
3079
3096
|
}
|
|
3080
3097
|
|
|
3081
3098
|
|
|
3099
|
+
def _read_project_id() -> str:
|
|
3100
|
+
"""Read project name from pyproject.toml in the current working directory."""
|
|
3101
|
+
pyproject_path = Path(os.getcwd()) / "pyproject.toml"
|
|
3102
|
+
if not pyproject_path.exists():
|
|
3103
|
+
raise RuntimeError(
|
|
3104
|
+
"Cannot determine project_id: pyproject.toml not found "
|
|
3105
|
+
"or missing [project].name"
|
|
3106
|
+
)
|
|
3107
|
+
with open(pyproject_path, "rb") as f:
|
|
3108
|
+
data = tomllib.load(f)
|
|
3109
|
+
project_name = data.get("project", {}).get("name")
|
|
3110
|
+
if not project_name:
|
|
3111
|
+
raise RuntimeError(
|
|
3112
|
+
"Cannot determine project_id: pyproject.toml not found "
|
|
3113
|
+
"or missing [project].name"
|
|
3114
|
+
)
|
|
3115
|
+
return project_name
|
|
3116
|
+
|
|
3117
|
+
|
|
3082
3118
|
def _dump_schema(app: App) -> dict:
|
|
3083
3119
|
"""Produce a JSON-serializable dict representing the app's command tree.
|
|
3084
3120
|
|
|
3085
3121
|
Fields whose values match the schema defaults are omitted.
|
|
3086
3122
|
The top-level ``defaults`` key documents what each missing field means.
|
|
3087
3123
|
"""
|
|
3124
|
+
project_id = _read_project_id()
|
|
3088
3125
|
schema: dict = {
|
|
3089
3126
|
"defaults": _build_schema_defaults(),
|
|
3127
|
+
"project_id": project_id,
|
|
3090
3128
|
"name": app.name,
|
|
3091
3129
|
"version": app.version,
|
|
3092
3130
|
"help": app.help,
|
|
@@ -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")
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
"""Tests for check discovery and double-entry registration (Phase 3)."""
|
|
2
2
|
|
|
3
|
-
import os
|
|
4
|
-
|
|
5
3
|
import pytest
|
|
6
4
|
|
|
7
5
|
import strictcli
|
|
8
6
|
|
|
9
7
|
|
|
10
8
|
VALID_TOML = """\
|
|
9
|
+
app = "testapp"
|
|
10
|
+
|
|
11
11
|
[checks.lint-code]
|
|
12
12
|
tags = ["code", "fast"]
|
|
13
13
|
severity = "error"
|
|
@@ -26,6 +26,8 @@ depends_on = ["lint-code"]
|
|
|
26
26
|
"""
|
|
27
27
|
|
|
28
28
|
INVALID_TOML = """\
|
|
29
|
+
app = "testapp"
|
|
30
|
+
|
|
29
31
|
[checks.BadName]
|
|
30
32
|
tags = ["a"]
|
|
31
33
|
severity = "error"
|
|
@@ -37,15 +39,15 @@ depends_on = []
|
|
|
37
39
|
|
|
38
40
|
|
|
39
41
|
class TestDiscovery:
|
|
40
|
-
def
|
|
42
|
+
def test_no_checks_path_ignores_cwd(self, tmp_path, monkeypatch):
|
|
43
|
+
"""Without checks_path, CWD checks.toml is NOT discovered."""
|
|
41
44
|
(tmp_path / ".strictcli").mkdir()
|
|
42
45
|
(tmp_path / ".strictcli" / "checks.toml").write_text(VALID_TOML)
|
|
43
46
|
monkeypatch.chdir(tmp_path)
|
|
44
47
|
|
|
45
48
|
app = strictcli.App(name="testapp", version="1.0.0", help="test app")
|
|
46
|
-
assert app._checks_enabled is
|
|
47
|
-
assert
|
|
48
|
-
assert "check-deps" in app._check_defs
|
|
49
|
+
assert app._checks_enabled is False
|
|
50
|
+
assert app._check_defs == {}
|
|
49
51
|
|
|
50
52
|
def test_no_toml_sets_checks_disabled(self, tmp_path, monkeypatch):
|
|
51
53
|
monkeypatch.chdir(tmp_path)
|
|
@@ -54,13 +56,15 @@ class TestDiscovery:
|
|
|
54
56
|
assert app._checks_enabled is False
|
|
55
57
|
assert app._check_defs == {}
|
|
56
58
|
|
|
57
|
-
def test_invalid_toml_raises_valueerror(self, tmp_path
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
monkeypatch.chdir(tmp_path)
|
|
59
|
+
def test_invalid_toml_raises_valueerror(self, tmp_path):
|
|
60
|
+
toml_file = tmp_path / "checks.toml"
|
|
61
|
+
toml_file.write_text(INVALID_TOML)
|
|
61
62
|
|
|
62
63
|
with pytest.raises(ValueError, match='invalid check name "BadName"'):
|
|
63
|
-
strictcli.App(
|
|
64
|
+
strictcli.App(
|
|
65
|
+
name="testapp", version="1.0.0", help="test app",
|
|
66
|
+
checks_path=str(toml_file),
|
|
67
|
+
)
|
|
64
68
|
|
|
65
69
|
|
|
66
70
|
class TestChecksPath:
|
|
@@ -116,8 +120,8 @@ class TestChecksPath:
|
|
|
116
120
|
checks_path=str(bad_path),
|
|
117
121
|
)
|
|
118
122
|
|
|
119
|
-
def
|
|
120
|
-
"""checks_path=None (default)
|
|
123
|
+
def test_none_means_disabled(self, tmp_path, monkeypatch):
|
|
124
|
+
"""checks_path=None (default) means checks are disabled."""
|
|
121
125
|
(tmp_path / ".strictcli").mkdir()
|
|
122
126
|
(tmp_path / ".strictcli" / "checks.toml").write_text(VALID_TOML)
|
|
123
127
|
monkeypatch.chdir(tmp_path)
|
|
@@ -126,17 +130,19 @@ class TestChecksPath:
|
|
|
126
130
|
name="testapp", version="1.0.0", help="test app",
|
|
127
131
|
checks_path=None,
|
|
128
132
|
)
|
|
129
|
-
assert app._checks_enabled is
|
|
130
|
-
assert
|
|
133
|
+
assert app._checks_enabled is False
|
|
134
|
+
assert app._check_defs == {}
|
|
131
135
|
|
|
132
136
|
|
|
133
137
|
class TestCheckDecorator:
|
|
134
|
-
def test_registers_implementation(self, tmp_path
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
monkeypatch.chdir(tmp_path)
|
|
138
|
+
def test_registers_implementation(self, tmp_path):
|
|
139
|
+
toml_file = tmp_path / "checks.toml"
|
|
140
|
+
toml_file.write_text(VALID_TOML)
|
|
138
141
|
|
|
139
|
-
app = strictcli.App(
|
|
142
|
+
app = strictcli.App(
|
|
143
|
+
name="testapp", version="1.0.0", help="test app",
|
|
144
|
+
checks_path=str(toml_file),
|
|
145
|
+
)
|
|
140
146
|
|
|
141
147
|
@app.check("lint-code")
|
|
142
148
|
def lint_impl(ctx):
|
|
@@ -144,24 +150,28 @@ class TestCheckDecorator:
|
|
|
144
150
|
|
|
145
151
|
assert app._check_defs["lint-code"].impl is lint_impl
|
|
146
152
|
|
|
147
|
-
def test_undeclared_name_raises(self, tmp_path
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
monkeypatch.chdir(tmp_path)
|
|
153
|
+
def test_undeclared_name_raises(self, tmp_path):
|
|
154
|
+
toml_file = tmp_path / "checks.toml"
|
|
155
|
+
toml_file.write_text(VALID_TOML)
|
|
151
156
|
|
|
152
|
-
app = strictcli.App(
|
|
157
|
+
app = strictcli.App(
|
|
158
|
+
name="testapp", version="1.0.0", help="test app",
|
|
159
|
+
checks_path=str(toml_file),
|
|
160
|
+
)
|
|
153
161
|
|
|
154
162
|
with pytest.raises(ValueError, match='cannot register check "nonexistent"'):
|
|
155
163
|
@app.check("nonexistent")
|
|
156
164
|
def bad_impl(ctx):
|
|
157
165
|
pass
|
|
158
166
|
|
|
159
|
-
def test_duplicate_registration_raises(self, tmp_path
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
monkeypatch.chdir(tmp_path)
|
|
167
|
+
def test_duplicate_registration_raises(self, tmp_path):
|
|
168
|
+
toml_file = tmp_path / "checks.toml"
|
|
169
|
+
toml_file.write_text(VALID_TOML)
|
|
163
170
|
|
|
164
|
-
app = strictcli.App(
|
|
171
|
+
app = strictcli.App(
|
|
172
|
+
name="testapp", version="1.0.0", help="test app",
|
|
173
|
+
checks_path=str(toml_file),
|
|
174
|
+
)
|
|
165
175
|
|
|
166
176
|
@app.check("lint-code")
|
|
167
177
|
def first(ctx):
|
|
@@ -177,19 +187,33 @@ class TestCheckDecorator:
|
|
|
177
187
|
|
|
178
188
|
app = strictcli.App(name="testapp", version="1.0.0", help="test app")
|
|
179
189
|
|
|
180
|
-
with pytest.raises(ValueError, match="
|
|
190
|
+
with pytest.raises(ValueError, match="checks not enabled"):
|
|
181
191
|
@app.check("anything")
|
|
182
192
|
def bad(ctx):
|
|
183
193
|
pass
|
|
184
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
|
+
|
|
185
207
|
|
|
186
208
|
class TestDoubleEntryValidation:
|
|
187
|
-
def test_missing_impl_error_in_test(self, tmp_path
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
monkeypatch.chdir(tmp_path)
|
|
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)
|
|
191
212
|
|
|
192
|
-
app = strictcli.App(
|
|
213
|
+
app = strictcli.App(
|
|
214
|
+
name="testapp", version="1.0.0", help="test app",
|
|
215
|
+
checks_path=str(toml_file),
|
|
216
|
+
)
|
|
193
217
|
|
|
194
218
|
# Register a dummy command so the app has something to run
|
|
195
219
|
@app.command("hello", help="say hello")
|
|
@@ -204,15 +228,17 @@ class TestDoubleEntryValidation:
|
|
|
204
228
|
# check-deps is not registered -- should fail validation
|
|
205
229
|
result = app.test(["hello"])
|
|
206
230
|
assert result.exit_code == 1
|
|
207
|
-
assert "checks declared in
|
|
231
|
+
assert "checks declared in checks.toml but not registered" in result.stderr
|
|
208
232
|
assert "check-deps" in result.stderr
|
|
209
233
|
|
|
210
|
-
def test_all_registered_passes_validation(self, tmp_path
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
monkeypatch.chdir(tmp_path)
|
|
234
|
+
def test_all_registered_passes_validation(self, tmp_path):
|
|
235
|
+
toml_file = tmp_path / "checks.toml"
|
|
236
|
+
toml_file.write_text(VALID_TOML)
|
|
214
237
|
|
|
215
|
-
app = strictcli.App(
|
|
238
|
+
app = strictcli.App(
|
|
239
|
+
name="testapp", version="1.0.0", help="test app",
|
|
240
|
+
checks_path=str(toml_file),
|
|
241
|
+
)
|
|
216
242
|
|
|
217
243
|
@app.command("hello", help="say hello")
|
|
218
244
|
def hello(**kw):
|
|
@@ -242,3 +268,28 @@ class TestDoubleEntryValidation:
|
|
|
242
268
|
result = app.test(["hello"])
|
|
243
269
|
assert result.exit_code == 0
|
|
244
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
|
+
)
|
|
@@ -113,7 +113,7 @@ class TestRunChecks:
|
|
|
113
113
|
def _make_app_with_checks(self, check_defs, tmp_path, monkeypatch):
|
|
114
114
|
"""Build an App with pre-populated check definitions."""
|
|
115
115
|
# Write a minimal TOML so the app discovers checks
|
|
116
|
-
toml_lines = []
|
|
116
|
+
toml_lines = ['app = "testapp"', ""]
|
|
117
117
|
for name, cdef in check_defs.items():
|
|
118
118
|
tags_str = ", ".join(f'"{t}"' for t in cdef.tags)
|
|
119
119
|
deps_str = ", ".join(f'"{d}"' for d in cdef.depends_on)
|
|
@@ -126,11 +126,13 @@ class TestRunChecks:
|
|
|
126
126
|
toml_lines.append(f"depends_on = [{deps_str}]")
|
|
127
127
|
toml_lines.append("")
|
|
128
128
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
monkeypatch.chdir(tmp_path)
|
|
129
|
+
toml_file = tmp_path / "checks.toml"
|
|
130
|
+
toml_file.write_text("\n".join(toml_lines))
|
|
132
131
|
|
|
133
|
-
app = strictcli.App(
|
|
132
|
+
app = strictcli.App(
|
|
133
|
+
name="testapp", version="1.0.0", help="test app",
|
|
134
|
+
checks_path=str(toml_file),
|
|
135
|
+
)
|
|
134
136
|
# Inject the impl functions from our check_defs
|
|
135
137
|
for name, cdef in check_defs.items():
|
|
136
138
|
if cdef.impl is not None:
|
|
@@ -2,10 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
|
|
5
|
+
import pytest
|
|
5
6
|
import strictcli
|
|
6
7
|
|
|
7
8
|
|
|
9
|
+
@pytest.fixture(autouse=True)
|
|
10
|
+
def _pyproject_in_tmp(tmp_path):
|
|
11
|
+
"""Ensure every test that uses tmp_path has a pyproject.toml for project_id."""
|
|
12
|
+
(tmp_path / "pyproject.toml").write_text('[project]\nname = "testproject"\n')
|
|
13
|
+
|
|
14
|
+
|
|
8
15
|
CHECKS_TOML = """\
|
|
16
|
+
app = "testapp"
|
|
17
|
+
|
|
9
18
|
[checks.lint-code]
|
|
10
19
|
tags = ["code", "fast"]
|
|
11
20
|
severity = "error"
|
|
@@ -28,11 +37,14 @@ class TestSchemaWithChecks:
|
|
|
28
37
|
"""--dump-schema includes checks key when checks are enabled."""
|
|
29
38
|
|
|
30
39
|
def test_checks_key_present(self, tmp_path, monkeypatch):
|
|
31
|
-
|
|
32
|
-
|
|
40
|
+
toml_file = tmp_path / "checks.toml"
|
|
41
|
+
toml_file.write_text(CHECKS_TOML)
|
|
33
42
|
monkeypatch.chdir(tmp_path)
|
|
34
43
|
|
|
35
|
-
app = strictcli.App(
|
|
44
|
+
app = strictcli.App(
|
|
45
|
+
name="testapp", version="1.0.0", help="test app",
|
|
46
|
+
checks_path=str(toml_file),
|
|
47
|
+
)
|
|
36
48
|
|
|
37
49
|
@app.check("lint-code")
|
|
38
50
|
def lint_impl(ctx):
|
|
@@ -54,11 +66,14 @@ class TestSchemaWithChecks:
|
|
|
54
66
|
assert len(data["checks"]) == 2
|
|
55
67
|
|
|
56
68
|
def test_check_metadata_correct(self, tmp_path, monkeypatch):
|
|
57
|
-
|
|
58
|
-
|
|
69
|
+
toml_file = tmp_path / "checks.toml"
|
|
70
|
+
toml_file.write_text(CHECKS_TOML)
|
|
59
71
|
monkeypatch.chdir(tmp_path)
|
|
60
72
|
|
|
61
|
-
app = strictcli.App(
|
|
73
|
+
app = strictcli.App(
|
|
74
|
+
name="testapp", version="1.0.0", help="test app",
|
|
75
|
+
checks_path=str(toml_file),
|
|
76
|
+
)
|
|
62
77
|
|
|
63
78
|
@app.check("lint-code")
|
|
64
79
|
def lint_impl(ctx):
|