strictcli 0.34.0__tar.gz → 0.35.2__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.34.0 → strictcli-0.35.2}/.rlsbl/config.json +1 -2
- {strictcli-0.34.0 → strictcli-0.35.2}/PKG-INFO +1 -1
- {strictcli-0.34.0 → strictcli-0.35.2}/pyproject.toml +1 -1
- strictcli-0.35.2/scripts/add_effect_classification.py +85 -0
- strictcli-0.35.2/scripts/add_forwarding_declaration.py +89 -0
- {strictcli-0.34.0 → strictcli-0.35.2}/strictcli/__init__.py +1937 -101
- {strictcli-0.34.0 → strictcli-0.35.2}/tests/conftest.py +16 -0
- strictcli-0.35.2/tests/flagship_app.py +98 -0
- {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_arg_default.py +9 -9
- {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_at_prefix.py +16 -16
- {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_auto_version.py +2 -2
- {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_call.py +42 -42
- {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_check_command.py +27 -10
- {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_check_discovery.py +3 -3
- {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_check_provider.py +21 -10
- {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_check_public_api.py +3 -2
- {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_check_schema.py +4 -4
- {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_choices.py +6 -6
- {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_choices_none.py +3 -3
- strictcli-0.35.2/tests/test_classification.py +167 -0
- {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_command_help_suggestion.py +2 -2
- {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_command_tags.py +41 -41
- {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_compound_types.py +47 -47
- {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_config.py +205 -77
- {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_config_fields.py +55 -55
- {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_config_file_path.py +1 -1
- {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_config_set_bugs.py +9 -9
- strictcli-0.35.2/tests/test_confirm.py +191 -0
- {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_connection_env.py +14 -14
- {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_context.py +22 -21
- {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_coverage.py +7 -7
- {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_deep_nesting.py +23 -23
- {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_dependencies.py +28 -28
- {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_deprecated.py +5 -5
- {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_dump_schema.py +78 -78
- {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_e2e.py +29 -29
- strictcli-0.35.2/tests/test_effects.py +1300 -0
- strictcli-0.35.2/tests/test_effects_bypass_check.py +385 -0
- {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_env.py +7 -7
- {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_exit_codes.py +1 -1
- {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_flag_sets.py +11 -11
- strictcli-0.35.2/tests/test_flagship_preview.py +131 -0
- {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_float_format.py +4 -4
- {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_float_type.py +11 -11
- {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_global_flag_conflict_position.py +1 -1
- {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_global_flags.py +66 -66
- strictcli-0.35.2/tests/test_guard_v2.py +183 -0
- {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_help.py +11 -11
- {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_hermetic.py +17 -17
- {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_infra_env.py +6 -6
- {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_int_type.py +7 -7
- {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_invoke.py +80 -80
- {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_keyword_flags.py +4 -4
- {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_mcp.py +43 -43
- {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_mutex.py +78 -78
- {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_nesting.py +2 -2
- {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_parser.py +25 -25
- {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_passthrough.py +23 -23
- {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_provenance.py +34 -34
- {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_provenance_phase2.py +30 -30
- {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_registration.py +18 -18
- {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_repeatable.py +23 -23
- {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_reserved_global_flags.py +1 -1
- strictcli-0.35.2/tests/test_reserved_quartet.py +207 -0
- {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_tool_export.py +95 -95
- {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_typed_args.py +45 -45
- {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_unique.py +5 -5
- {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_validate.py +4 -4
- {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_variadic.py +10 -10
- {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_visibility.py +32 -32
- {strictcli-0.34.0 → strictcli-0.35.2}/uv.lock +1 -1
- {strictcli-0.34.0 → strictcli-0.35.2}/.claude/settings.json +0 -0
- {strictcli-0.34.0 → strictcli-0.35.2}/.github/workflows/ci.yml +0 -0
- {strictcli-0.34.0 → strictcli-0.35.2}/.github/workflows/publish.yml +0 -0
- {strictcli-0.34.0 → strictcli-0.35.2}/.gitignore +0 -0
- {strictcli-0.34.0 → strictcli-0.35.2}/.rlsbl/bases/.github/workflows/ci.yml +0 -0
- {strictcli-0.34.0 → strictcli-0.35.2}/.rlsbl/bases/.github/workflows/publish.yml +0 -0
- {strictcli-0.34.0 → strictcli-0.35.2}/.rlsbl/bases/.gitignore +0 -0
- {strictcli-0.34.0 → strictcli-0.35.2}/.rlsbl/bases/.rlsbl/lint/python.toml +0 -0
- {strictcli-0.34.0 → strictcli-0.35.2}/.rlsbl/lint/python.toml +0 -0
- {strictcli-0.34.0 → strictcli-0.35.2}/.rlsbl/managed-files.json +0 -0
- {strictcli-0.34.0 → strictcli-0.35.2}/.rlsbl/version +0 -0
- {strictcli-0.34.0 → strictcli-0.35.2}/.strictcli/schema.json +0 -0
- {strictcli-0.34.0 → strictcli-0.35.2}/CLAUDE.md +0 -0
- {strictcli-0.34.0 → strictcli-0.35.2}/LICENSE +0 -0
- {strictcli-0.34.0 → strictcli-0.35.2}/README.md +0 -0
- {strictcli-0.34.0 → strictcli-0.35.2}/strictcli/py.typed +0 -0
- {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_arg_default_validation.py +0 -0
- {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_check_runner.py +0 -0
- {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_check_types.py +0 -0
- {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_float_vectors.py +0 -0
- {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_tagdsl.py +0 -0
- {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_toml_loading.py +0 -0
- {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_utilities.py +0 -0
- {strictcli-0.34.0 → strictcli-0.35.2}/todo/.defer/deferred.md +0 -0
- {strictcli-0.34.0 → strictcli-0.35.2}/todo/.done/keyword-collision-in-flag-param-name.md +0 -0
- {strictcli-0.34.0 → strictcli-0.35.2}/todo/.done/original-idea.md +0 -0
- {strictcli-0.34.0 → strictcli-0.35.2}/todo/.done/public-check-runner-api.md +0 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Mechanical sweep: add the mandatory ``effect=`` classification to command
|
|
3
|
+
registrations that lack it.
|
|
4
|
+
|
|
5
|
+
The effects regime makes ``effect="read_only" | "mutating"`` mandatory on every
|
|
6
|
+
command. This script rewrites ``.command(...)`` call sites that do not already
|
|
7
|
+
declare it, inserting the keyword immediately before the (always present,
|
|
8
|
+
always keyword-passed) ``help=`` argument so the result stays readable:
|
|
9
|
+
|
|
10
|
+
@app.command("deploy", effect="read_only", help="deploy the app")
|
|
11
|
+
|
|
12
|
+
Only ``.command(...)`` attribute calls are touched; ``.group(...)``,
|
|
13
|
+
``.deprecate(...)`` and everything else are left alone. Files are rewritten in
|
|
14
|
+
place. Re-running is a no-op (sites that already declare ``effect=`` are
|
|
15
|
+
skipped).
|
|
16
|
+
|
|
17
|
+
Usage:
|
|
18
|
+
scripts/add_effect_classification.py [--effect read_only] FILE [FILE ...]
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import argparse
|
|
24
|
+
import ast
|
|
25
|
+
import sys
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _insertions(source: str, effect: str) -> list[tuple[int, int, str]]:
|
|
30
|
+
"""Return (lineno, col_offset, text) insertion points, one per call site."""
|
|
31
|
+
tree = ast.parse(source)
|
|
32
|
+
points: list[tuple[int, int, str]] = []
|
|
33
|
+
for node in ast.walk(tree):
|
|
34
|
+
if not isinstance(node, ast.Call):
|
|
35
|
+
continue
|
|
36
|
+
func = node.func
|
|
37
|
+
if not isinstance(func, ast.Attribute) or func.attr != "command":
|
|
38
|
+
continue
|
|
39
|
+
kw_names = {kw.arg for kw in node.keywords}
|
|
40
|
+
if "effect" in kw_names:
|
|
41
|
+
continue
|
|
42
|
+
help_kw = next((kw for kw in node.keywords if kw.arg == "help"), None)
|
|
43
|
+
if help_kw is None:
|
|
44
|
+
raise SystemExit(
|
|
45
|
+
f"command registration without help= at line {node.lineno}; "
|
|
46
|
+
"insertion anchor missing"
|
|
47
|
+
)
|
|
48
|
+
points.append((help_kw.lineno, help_kw.col_offset, f'effect="{effect}", '))
|
|
49
|
+
return points
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def rewrite(path: Path, effect: str) -> int:
|
|
53
|
+
source = path.read_text()
|
|
54
|
+
points = _insertions(source, effect)
|
|
55
|
+
if not points:
|
|
56
|
+
return 0
|
|
57
|
+
lines = source.splitlines(keepends=True)
|
|
58
|
+
# Apply bottom-up so earlier offsets stay valid.
|
|
59
|
+
for lineno, col, text in sorted(points, reverse=True):
|
|
60
|
+
line = lines[lineno - 1]
|
|
61
|
+
lines[lineno - 1] = line[:col] + text + line[col:]
|
|
62
|
+
path.write_text("".join(lines))
|
|
63
|
+
return len(points)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def main(argv: list[str]) -> int:
|
|
67
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
68
|
+
parser.add_argument("--effect", default="read_only",
|
|
69
|
+
choices=["read_only", "mutating"],
|
|
70
|
+
help="classification to insert (default: read_only)")
|
|
71
|
+
parser.add_argument("files", nargs="+", type=Path)
|
|
72
|
+
ns = parser.parse_args(argv)
|
|
73
|
+
|
|
74
|
+
total = 0
|
|
75
|
+
for path in ns.files:
|
|
76
|
+
n = rewrite(path, ns.effect)
|
|
77
|
+
if n:
|
|
78
|
+
print(f"{path}: {n} registration(s)")
|
|
79
|
+
total += n
|
|
80
|
+
print(f"total: {total}")
|
|
81
|
+
return 0
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
if __name__ == "__main__":
|
|
85
|
+
sys.exit(main(sys.argv[1:]))
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Mechanical sweep: declare forwarding on commands whose handler takes ``**kwargs``.
|
|
3
|
+
|
|
4
|
+
Guard v2 removed the blanket ``**kwargs`` exemption from handler-signature
|
|
5
|
+
validation: a var-keyword handler is a registration error unless the command
|
|
6
|
+
declares forwarding. This script finds ``@x.command(...)`` decorators applied to
|
|
7
|
+
a function with a ``**kwargs`` parameter and inserts
|
|
8
|
+
|
|
9
|
+
forwarding=strictcli.Forwarding(reason="<reason>"),
|
|
10
|
+
|
|
11
|
+
immediately before the ``help=`` argument.
|
|
12
|
+
|
|
13
|
+
Passthrough registrations are skipped: a passthrough handler's signature is
|
|
14
|
+
deliberately unpoliced, so it never needs the declaration. Sites that already
|
|
15
|
+
declare ``forwarding=`` are skipped, so re-running is a no-op.
|
|
16
|
+
|
|
17
|
+
Usage:
|
|
18
|
+
scripts/add_forwarding_declaration.py --reason "why" FILE [FILE ...]
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import argparse
|
|
24
|
+
import ast
|
|
25
|
+
import sys
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _insertions(source: str, reason: str, qualifier: str) -> list[tuple[int, int, str]]:
|
|
30
|
+
tree = ast.parse(source)
|
|
31
|
+
points: list[tuple[int, int, str]] = []
|
|
32
|
+
for node in ast.walk(tree):
|
|
33
|
+
if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
34
|
+
continue
|
|
35
|
+
if node.args.kwarg is None:
|
|
36
|
+
continue
|
|
37
|
+
for dec in node.decorator_list:
|
|
38
|
+
if not isinstance(dec, ast.Call):
|
|
39
|
+
continue
|
|
40
|
+
func = dec.func
|
|
41
|
+
if not isinstance(func, ast.Attribute) or func.attr != "command":
|
|
42
|
+
continue
|
|
43
|
+
kw_names = {kw.arg for kw in dec.keywords}
|
|
44
|
+
if "forwarding" in kw_names or "passthrough" in kw_names:
|
|
45
|
+
continue
|
|
46
|
+
help_kw = next((kw for kw in dec.keywords if kw.arg == "help"), None)
|
|
47
|
+
if help_kw is None:
|
|
48
|
+
raise SystemExit(
|
|
49
|
+
f"command registration without help= at line {dec.lineno}"
|
|
50
|
+
)
|
|
51
|
+
text = f'forwarding={qualifier}Forwarding(reason="{reason}"), '
|
|
52
|
+
points.append((help_kw.lineno, help_kw.col_offset, text))
|
|
53
|
+
return points
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def rewrite(path: Path, reason: str, qualifier: str) -> int:
|
|
57
|
+
source = path.read_text()
|
|
58
|
+
points = _insertions(source, reason, qualifier)
|
|
59
|
+
if not points:
|
|
60
|
+
return 0
|
|
61
|
+
lines = source.splitlines(keepends=True)
|
|
62
|
+
for lineno, col, text in sorted(points, reverse=True):
|
|
63
|
+
line = lines[lineno - 1]
|
|
64
|
+
lines[lineno - 1] = line[:col] + text + line[col:]
|
|
65
|
+
path.write_text("".join(lines))
|
|
66
|
+
return len(points)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def main(argv: list[str]) -> int:
|
|
70
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
71
|
+
parser.add_argument("--reason", required=True,
|
|
72
|
+
help="the mandatory forwarding reason string")
|
|
73
|
+
parser.add_argument("--qualifier", default="strictcli.",
|
|
74
|
+
help='module qualifier for Forwarding (default "strictcli.")')
|
|
75
|
+
parser.add_argument("files", nargs="+", type=Path)
|
|
76
|
+
ns = parser.parse_args(argv)
|
|
77
|
+
|
|
78
|
+
total = 0
|
|
79
|
+
for path in ns.files:
|
|
80
|
+
n = rewrite(path, ns.reason, ns.qualifier)
|
|
81
|
+
if n:
|
|
82
|
+
print(f"{path}: {n} registration(s)")
|
|
83
|
+
total += n
|
|
84
|
+
print(f"total: {total}")
|
|
85
|
+
return 0
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
if __name__ == "__main__":
|
|
89
|
+
sys.exit(main(sys.argv[1:]))
|