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.
Files changed (98) hide show
  1. {strictcli-0.34.0 → strictcli-0.35.2}/.rlsbl/config.json +1 -2
  2. {strictcli-0.34.0 → strictcli-0.35.2}/PKG-INFO +1 -1
  3. {strictcli-0.34.0 → strictcli-0.35.2}/pyproject.toml +1 -1
  4. strictcli-0.35.2/scripts/add_effect_classification.py +85 -0
  5. strictcli-0.35.2/scripts/add_forwarding_declaration.py +89 -0
  6. {strictcli-0.34.0 → strictcli-0.35.2}/strictcli/__init__.py +1937 -101
  7. {strictcli-0.34.0 → strictcli-0.35.2}/tests/conftest.py +16 -0
  8. strictcli-0.35.2/tests/flagship_app.py +98 -0
  9. {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_arg_default.py +9 -9
  10. {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_at_prefix.py +16 -16
  11. {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_auto_version.py +2 -2
  12. {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_call.py +42 -42
  13. {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_check_command.py +27 -10
  14. {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_check_discovery.py +3 -3
  15. {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_check_provider.py +21 -10
  16. {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_check_public_api.py +3 -2
  17. {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_check_schema.py +4 -4
  18. {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_choices.py +6 -6
  19. {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_choices_none.py +3 -3
  20. strictcli-0.35.2/tests/test_classification.py +167 -0
  21. {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_command_help_suggestion.py +2 -2
  22. {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_command_tags.py +41 -41
  23. {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_compound_types.py +47 -47
  24. {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_config.py +205 -77
  25. {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_config_fields.py +55 -55
  26. {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_config_file_path.py +1 -1
  27. {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_config_set_bugs.py +9 -9
  28. strictcli-0.35.2/tests/test_confirm.py +191 -0
  29. {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_connection_env.py +14 -14
  30. {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_context.py +22 -21
  31. {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_coverage.py +7 -7
  32. {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_deep_nesting.py +23 -23
  33. {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_dependencies.py +28 -28
  34. {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_deprecated.py +5 -5
  35. {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_dump_schema.py +78 -78
  36. {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_e2e.py +29 -29
  37. strictcli-0.35.2/tests/test_effects.py +1300 -0
  38. strictcli-0.35.2/tests/test_effects_bypass_check.py +385 -0
  39. {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_env.py +7 -7
  40. {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_exit_codes.py +1 -1
  41. {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_flag_sets.py +11 -11
  42. strictcli-0.35.2/tests/test_flagship_preview.py +131 -0
  43. {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_float_format.py +4 -4
  44. {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_float_type.py +11 -11
  45. {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_global_flag_conflict_position.py +1 -1
  46. {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_global_flags.py +66 -66
  47. strictcli-0.35.2/tests/test_guard_v2.py +183 -0
  48. {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_help.py +11 -11
  49. {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_hermetic.py +17 -17
  50. {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_infra_env.py +6 -6
  51. {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_int_type.py +7 -7
  52. {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_invoke.py +80 -80
  53. {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_keyword_flags.py +4 -4
  54. {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_mcp.py +43 -43
  55. {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_mutex.py +78 -78
  56. {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_nesting.py +2 -2
  57. {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_parser.py +25 -25
  58. {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_passthrough.py +23 -23
  59. {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_provenance.py +34 -34
  60. {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_provenance_phase2.py +30 -30
  61. {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_registration.py +18 -18
  62. {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_repeatable.py +23 -23
  63. {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_reserved_global_flags.py +1 -1
  64. strictcli-0.35.2/tests/test_reserved_quartet.py +207 -0
  65. {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_tool_export.py +95 -95
  66. {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_typed_args.py +45 -45
  67. {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_unique.py +5 -5
  68. {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_validate.py +4 -4
  69. {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_variadic.py +10 -10
  70. {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_visibility.py +32 -32
  71. {strictcli-0.34.0 → strictcli-0.35.2}/uv.lock +1 -1
  72. {strictcli-0.34.0 → strictcli-0.35.2}/.claude/settings.json +0 -0
  73. {strictcli-0.34.0 → strictcli-0.35.2}/.github/workflows/ci.yml +0 -0
  74. {strictcli-0.34.0 → strictcli-0.35.2}/.github/workflows/publish.yml +0 -0
  75. {strictcli-0.34.0 → strictcli-0.35.2}/.gitignore +0 -0
  76. {strictcli-0.34.0 → strictcli-0.35.2}/.rlsbl/bases/.github/workflows/ci.yml +0 -0
  77. {strictcli-0.34.0 → strictcli-0.35.2}/.rlsbl/bases/.github/workflows/publish.yml +0 -0
  78. {strictcli-0.34.0 → strictcli-0.35.2}/.rlsbl/bases/.gitignore +0 -0
  79. {strictcli-0.34.0 → strictcli-0.35.2}/.rlsbl/bases/.rlsbl/lint/python.toml +0 -0
  80. {strictcli-0.34.0 → strictcli-0.35.2}/.rlsbl/lint/python.toml +0 -0
  81. {strictcli-0.34.0 → strictcli-0.35.2}/.rlsbl/managed-files.json +0 -0
  82. {strictcli-0.34.0 → strictcli-0.35.2}/.rlsbl/version +0 -0
  83. {strictcli-0.34.0 → strictcli-0.35.2}/.strictcli/schema.json +0 -0
  84. {strictcli-0.34.0 → strictcli-0.35.2}/CLAUDE.md +0 -0
  85. {strictcli-0.34.0 → strictcli-0.35.2}/LICENSE +0 -0
  86. {strictcli-0.34.0 → strictcli-0.35.2}/README.md +0 -0
  87. {strictcli-0.34.0 → strictcli-0.35.2}/strictcli/py.typed +0 -0
  88. {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_arg_default_validation.py +0 -0
  89. {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_check_runner.py +0 -0
  90. {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_check_types.py +0 -0
  91. {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_float_vectors.py +0 -0
  92. {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_tagdsl.py +0 -0
  93. {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_toml_loading.py +0 -0
  94. {strictcli-0.34.0 → strictcli-0.35.2}/tests/test_utilities.py +0 -0
  95. {strictcli-0.34.0 → strictcli-0.35.2}/todo/.defer/deferred.md +0 -0
  96. {strictcli-0.34.0 → strictcli-0.35.2}/todo/.done/keyword-collision-in-flag-param-name.md +0 -0
  97. {strictcli-0.34.0 → strictcli-0.35.2}/todo/.done/original-idea.md +0 -0
  98. {strictcli-0.34.0 → strictcli-0.35.2}/todo/.done/public-check-runner-api.md +0 -0
@@ -10,6 +10,5 @@
10
10
  "target": "pypi",
11
11
  "local": false
12
12
  }
13
- },
14
- "coverage_unit": "commit"
13
+ }
15
14
  }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: strictcli
3
- Version: 0.34.0
3
+ Version: 0.35.2
4
4
  Summary: A strict CLI framework for Python
5
5
  Project-URL: Homepage, https://github.com/smm-h/strictcli
6
6
  Project-URL: Repository, https://github.com/smm-h/strictcli
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "strictcli"
7
- version = "0.34.0"
7
+ version = "0.35.2"
8
8
  description = "A strict CLI framework for Python"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -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:]))