strictcli 0.22.0__tar.gz → 0.24.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.22.0 → strictcli-0.24.0}/CHANGELOG.md +9 -3
- {strictcli-0.22.0 → strictcli-0.24.0}/PKG-INFO +1 -1
- {strictcli-0.22.0 → strictcli-0.24.0}/package-lock.json +2 -2
- {strictcli-0.22.0 → strictcli-0.24.0}/package.json +1 -1
- {strictcli-0.22.0 → strictcli-0.24.0}/pyproject.toml +1 -1
- {strictcli-0.22.0 → strictcli-0.24.0}/strictcli/__init__.py +110 -78
- strictcli-0.24.0/tests/test_arg_default_validation.py +79 -0
- {strictcli-0.22.0 → strictcli-0.24.0}/tests/test_check_runner.py +64 -2
- strictcli-0.24.0/tests/test_choices_none.py +163 -0
- {strictcli-0.22.0 → strictcli-0.24.0}/tests/test_dump_schema.py +21 -21
- {strictcli-0.22.0 → strictcli-0.24.0}/tests/test_e2e.py +1 -1
- {strictcli-0.22.0 → strictcli-0.24.0}/tests/test_help.py +3 -3
- {strictcli-0.22.0 → strictcli-0.24.0}/uv.lock +1 -1
- {strictcli-0.22.0 → strictcli-0.24.0}/.claude/settings.json +0 -0
- {strictcli-0.22.0 → strictcli-0.24.0}/.github/workflows/ci.yml +0 -0
- {strictcli-0.22.0 → strictcli-0.24.0}/.github/workflows/publish.yml +0 -0
- {strictcli-0.22.0 → strictcli-0.24.0}/.gitignore +0 -0
- {strictcli-0.22.0 → strictcli-0.24.0}/.npmignore +0 -0
- {strictcli-0.22.0 → strictcli-0.24.0}/.rlsbl/config.json +0 -0
- {strictcli-0.22.0 → strictcli-0.24.0}/.rlsbl/hashes.json +0 -0
- {strictcli-0.22.0 → strictcli-0.24.0}/.rlsbl/version +0 -0
- {strictcli-0.22.0 → strictcli-0.24.0}/.strictcli/schema.json +0 -0
- {strictcli-0.22.0 → strictcli-0.24.0}/CLAUDE.md +0 -0
- {strictcli-0.22.0 → strictcli-0.24.0}/LICENSE +0 -0
- {strictcli-0.22.0 → strictcli-0.24.0}/README.md +0 -0
- {strictcli-0.22.0 → strictcli-0.24.0}/index.js +0 -0
- {strictcli-0.22.0 → strictcli-0.24.0}/postinstall.js +0 -0
- {strictcli-0.22.0 → strictcli-0.24.0}/tests/test_arg_default.py +0 -0
- {strictcli-0.22.0 → strictcli-0.24.0}/tests/test_at_prefix.py +0 -0
- {strictcli-0.22.0 → strictcli-0.24.0}/tests/test_auto_version.py +0 -0
- {strictcli-0.22.0 → strictcli-0.24.0}/tests/test_call.py +0 -0
- {strictcli-0.22.0 → strictcli-0.24.0}/tests/test_check_command.py +0 -0
- {strictcli-0.22.0 → strictcli-0.24.0}/tests/test_check_discovery.py +0 -0
- {strictcli-0.22.0 → strictcli-0.24.0}/tests/test_check_public_api.py +0 -0
- {strictcli-0.22.0 → strictcli-0.24.0}/tests/test_check_schema.py +0 -0
- {strictcli-0.22.0 → strictcli-0.24.0}/tests/test_check_types.py +0 -0
- {strictcli-0.22.0 → strictcli-0.24.0}/tests/test_choices.py +0 -0
- {strictcli-0.22.0 → strictcli-0.24.0}/tests/test_command_help_suggestion.py +0 -0
- {strictcli-0.22.0 → strictcli-0.24.0}/tests/test_command_tags.py +0 -0
- {strictcli-0.22.0 → strictcli-0.24.0}/tests/test_compound_types.py +0 -0
- {strictcli-0.22.0 → strictcli-0.24.0}/tests/test_config.py +0 -0
- {strictcli-0.22.0 → strictcli-0.24.0}/tests/test_config_fields.py +0 -0
- {strictcli-0.22.0 → strictcli-0.24.0}/tests/test_config_file_path.py +0 -0
- {strictcli-0.22.0 → strictcli-0.24.0}/tests/test_config_set_bugs.py +0 -0
- {strictcli-0.22.0 → strictcli-0.24.0}/tests/test_context.py +0 -0
- {strictcli-0.22.0 → strictcli-0.24.0}/tests/test_deep_nesting.py +0 -0
- {strictcli-0.22.0 → strictcli-0.24.0}/tests/test_dependencies.py +0 -0
- {strictcli-0.22.0 → strictcli-0.24.0}/tests/test_deprecated.py +0 -0
- {strictcli-0.22.0 → strictcli-0.24.0}/tests/test_env.py +0 -0
- {strictcli-0.22.0 → strictcli-0.24.0}/tests/test_exit_codes.py +0 -0
- {strictcli-0.22.0 → strictcli-0.24.0}/tests/test_flag_sets.py +0 -0
- {strictcli-0.22.0 → strictcli-0.24.0}/tests/test_float_type.py +0 -0
- {strictcli-0.22.0 → strictcli-0.24.0}/tests/test_global_flags.py +0 -0
- {strictcli-0.22.0 → strictcli-0.24.0}/tests/test_int_type.py +0 -0
- {strictcli-0.22.0 → strictcli-0.24.0}/tests/test_invoke.py +0 -0
- {strictcli-0.22.0 → strictcli-0.24.0}/tests/test_keyword_flags.py +0 -0
- {strictcli-0.22.0 → strictcli-0.24.0}/tests/test_mcp.py +0 -0
- {strictcli-0.22.0 → strictcli-0.24.0}/tests/test_mutex.py +0 -0
- {strictcli-0.22.0 → strictcli-0.24.0}/tests/test_nesting.py +0 -0
- {strictcli-0.22.0 → strictcli-0.24.0}/tests/test_parser.py +0 -0
- {strictcli-0.22.0 → strictcli-0.24.0}/tests/test_passthrough.py +0 -0
- {strictcli-0.22.0 → strictcli-0.24.0}/tests/test_registration.py +0 -0
- {strictcli-0.22.0 → strictcli-0.24.0}/tests/test_repeatable.py +0 -0
- {strictcli-0.22.0 → strictcli-0.24.0}/tests/test_tagdsl.py +0 -0
- {strictcli-0.22.0 → strictcli-0.24.0}/tests/test_toml_loading.py +0 -0
- {strictcli-0.22.0 → strictcli-0.24.0}/tests/test_tool_export.py +0 -0
- {strictcli-0.22.0 → strictcli-0.24.0}/tests/test_typed_args.py +0 -0
- {strictcli-0.22.0 → strictcli-0.24.0}/tests/test_unique.py +0 -0
- {strictcli-0.22.0 → strictcli-0.24.0}/tests/test_utilities.py +0 -0
- {strictcli-0.22.0 → strictcli-0.24.0}/tests/test_validate.py +0 -0
- {strictcli-0.22.0 → strictcli-0.24.0}/tests/test_variadic.py +0 -0
- {strictcli-0.22.0 → strictcli-0.24.0}/tests/test_visibility.py +0 -0
- {strictcli-0.22.0 → strictcli-0.24.0}/todo/.defer/deferred.md +0 -0
- {strictcli-0.22.0 → strictcli-0.24.0}/todo/.done/keyword-collision-in-flag-param-name.md +0 -0
- {strictcli-0.22.0 → strictcli-0.24.0}/todo/.done/original-idea.md +0 -0
- {strictcli-0.22.0 → strictcli-0.24.0}/todo/.done/public-check-runner-api.md +0 -0
|
@@ -2,17 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
# Changelog
|
|
4
4
|
|
|
5
|
-
## 0.
|
|
5
|
+
## 0.23.0
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Registration-time ban on bare --force flag name and --no-* prefix
|
|
8
8
|
|
|
9
9
|
<details>
|
|
10
10
|
<summary>Context</summary>
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
Flag names 'force' (exact) and names starting with 'no-' are now rejected at registration time. Bare --force encourages agents to bypass guardrails without thinking; qualified names like --force-overwrite make the intent explicit. The no- prefix is reserved for strictcli's auto-generated negation system (--no-flag for negatable bools).
|
|
13
13
|
|
|
14
14
|
</details>
|
|
15
15
|
|
|
16
|
+
### Features
|
|
17
|
+
|
|
18
|
+
- **New.** Registration-time ban on bare --force flag name and --no-* prefix. Flag names starting with no- are reserved for the negation system.
|
|
19
|
+
|
|
20
|
+
## 0.22.0
|
|
21
|
+
|
|
16
22
|
### Breaking
|
|
17
23
|
|
|
18
24
|
- **Breaking.** Bool flags no longer auto-default to false. BoolFlag without explicit Default(false) is now required — user must pass --flag or --no-flag.
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "strictcli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "strictcli",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.24.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.24.0"
|
|
6
6
|
|
|
7
7
|
__all__ = [
|
|
8
8
|
"App", "Flag", "Arg", "FlagSet", "MutexGroup", "CoRequired", "Requires",
|
|
@@ -1204,6 +1204,17 @@ class Flag:
|
|
|
1204
1204
|
|
|
1205
1205
|
def __post_init__(self) -> None:
|
|
1206
1206
|
_require_non_empty_str(self.help, "help", "Flag")
|
|
1207
|
+
if self.name == "force":
|
|
1208
|
+
raise ValueError(
|
|
1209
|
+
"flag 'force' is a reserved name; use a qualified name "
|
|
1210
|
+
"like 'force-overwrite' or 'force-delete'"
|
|
1211
|
+
)
|
|
1212
|
+
if self.name.startswith("no-"):
|
|
1213
|
+
raise ValueError(
|
|
1214
|
+
f"flag '{self.name}': names starting with 'no-' are "
|
|
1215
|
+
f"reserved for the negation system; use a positive "
|
|
1216
|
+
f"name instead"
|
|
1217
|
+
)
|
|
1207
1218
|
|
|
1208
1219
|
# Parse compound types (list[T], dict[str, T])
|
|
1209
1220
|
kind, item_t, val_t = _parse_compound_type(
|
|
@@ -1473,7 +1484,38 @@ class Arg:
|
|
|
1473
1484
|
)
|
|
1474
1485
|
# Validate default type matches declared type
|
|
1475
1486
|
if not isinstance(self.default, _MissingSentinel) and self.default is not None:
|
|
1476
|
-
if self.
|
|
1487
|
+
if self.compound == "list":
|
|
1488
|
+
# self.type was normalized to the item type above; the
|
|
1489
|
+
# default itself must be a list of that item type.
|
|
1490
|
+
if not isinstance(self.default, list):
|
|
1491
|
+
raise ValueError(
|
|
1492
|
+
f'Arg "{self.name}": list arg default must be a list'
|
|
1493
|
+
)
|
|
1494
|
+
if len(self.default) == 0:
|
|
1495
|
+
raise ValueError(
|
|
1496
|
+
f'Arg "{self.name}": explicit empty default is '
|
|
1497
|
+
f"redundant for list args, omit the default"
|
|
1498
|
+
)
|
|
1499
|
+
type_name = {str: "str", int: "int", float: "float"}[self.type]
|
|
1500
|
+
for i, elem in enumerate(self.default):
|
|
1501
|
+
if self.type is str:
|
|
1502
|
+
valid = isinstance(elem, str)
|
|
1503
|
+
elif self.type is int:
|
|
1504
|
+
valid = isinstance(elem, int) and not isinstance(elem, bool)
|
|
1505
|
+
else: # float
|
|
1506
|
+
valid = (
|
|
1507
|
+
isinstance(elem, (int, float))
|
|
1508
|
+
and not isinstance(elem, bool)
|
|
1509
|
+
)
|
|
1510
|
+
if valid and isinstance(elem, int):
|
|
1511
|
+
# Auto-coerce int to float, mirroring list flag defaults
|
|
1512
|
+
self.default[i] = float(elem)
|
|
1513
|
+
if not valid:
|
|
1514
|
+
raise ValueError(
|
|
1515
|
+
f'Arg "{self.name}": default element {i} '
|
|
1516
|
+
f"is not of type {type_name}"
|
|
1517
|
+
)
|
|
1518
|
+
elif self.type is int:
|
|
1477
1519
|
if not isinstance(self.default, int) or isinstance(self.default, bool):
|
|
1478
1520
|
raise ValueError(
|
|
1479
1521
|
f'Arg "{self.name}": type=int requires an int default, '
|
|
@@ -1491,6 +1533,12 @@ class Arg:
|
|
|
1491
1533
|
f'Arg "{self.name}": type=bool requires a bool default, '
|
|
1492
1534
|
f"got {type(self.default).__name__!r}"
|
|
1493
1535
|
)
|
|
1536
|
+
elif self.type is str:
|
|
1537
|
+
if not isinstance(self.default, str):
|
|
1538
|
+
raise ValueError(
|
|
1539
|
+
f'Arg "{self.name}": type=str requires a str default, '
|
|
1540
|
+
f"got {type(self.default).__name__!r}"
|
|
1541
|
+
)
|
|
1494
1542
|
# Validate default is in choices
|
|
1495
1543
|
if self.choices is not None and not isinstance(self.default, _MissingSentinel) and self.default is not None:
|
|
1496
1544
|
if self.default not in self.choices:
|
|
@@ -3174,21 +3222,8 @@ class App:
|
|
|
3174
3222
|
|
|
3175
3223
|
# Validate choices for global flags
|
|
3176
3224
|
for f in self._global_flags:
|
|
3177
|
-
if f.
|
|
3178
|
-
|
|
3179
|
-
for val in cli_set[f.name]:
|
|
3180
|
-
if val not in f.choices:
|
|
3181
|
-
choices_str = ", ".join(str(c) for c in f.choices)
|
|
3182
|
-
raise _ParseError(
|
|
3183
|
-
f"--{f.name}: invalid value '{val}', must be one of: {choices_str}"
|
|
3184
|
-
)
|
|
3185
|
-
else:
|
|
3186
|
-
val = cli_set[f.name]
|
|
3187
|
-
if val not in f.choices:
|
|
3188
|
-
choices_str = ", ".join(str(c) for c in f.choices)
|
|
3189
|
-
raise _ParseError(
|
|
3190
|
-
f"--{f.name}: invalid value '{val}', must be one of: {choices_str}"
|
|
3191
|
-
)
|
|
3225
|
+
if f.name in cli_set:
|
|
3226
|
+
_validate_choices(f.name, cli_set[f.name], f.repeatable, f.choices)
|
|
3192
3227
|
|
|
3193
3228
|
return cli_set, remaining
|
|
3194
3229
|
|
|
@@ -3743,6 +3778,39 @@ def _tokens_contain_help(tokens: list[str]) -> bool:
|
|
|
3743
3778
|
return False
|
|
3744
3779
|
|
|
3745
3780
|
|
|
3781
|
+
def _validate_choices(
|
|
3782
|
+
name: str,
|
|
3783
|
+
val: object,
|
|
3784
|
+
repeatable: bool,
|
|
3785
|
+
choices: list | None,
|
|
3786
|
+
*,
|
|
3787
|
+
is_arg: bool = False,
|
|
3788
|
+
) -> None:
|
|
3789
|
+
"""Validate a resolved flag or arg value against its choices list.
|
|
3790
|
+
|
|
3791
|
+
Raises _ParseError on an invalid value. is_arg selects the message prefix
|
|
3792
|
+
("argument 'name':" instead of "--name:"); the two f-strings are kept as
|
|
3793
|
+
full literals so conformance/check_error_parity.py can extract them.
|
|
3794
|
+
A None value is exempt from validation: None only arises when the flag or
|
|
3795
|
+
arg was not passed (an unset mutex flag, or default=None on an arg) -- a
|
|
3796
|
+
CLI-supplied value is never None.
|
|
3797
|
+
"""
|
|
3798
|
+
if choices is None or val is None:
|
|
3799
|
+
return
|
|
3800
|
+
vals = val if repeatable else [val]
|
|
3801
|
+
for v in vals:
|
|
3802
|
+
if v not in choices:
|
|
3803
|
+
choices_str = ", ".join(str(c) for c in choices)
|
|
3804
|
+
if is_arg:
|
|
3805
|
+
raise _ParseError(
|
|
3806
|
+
f"argument '{name}': invalid value '{v}', "
|
|
3807
|
+
f"must be one of: {choices_str}"
|
|
3808
|
+
)
|
|
3809
|
+
raise _ParseError(
|
|
3810
|
+
f"--{name}: invalid value '{v}', must be one of: {choices_str}"
|
|
3811
|
+
)
|
|
3812
|
+
|
|
3813
|
+
|
|
3746
3814
|
def _validate_and_build_kwargs(
|
|
3747
3815
|
cmd: Command,
|
|
3748
3816
|
cli_set: dict[str, object],
|
|
@@ -3838,21 +3906,8 @@ def _validate_and_build_kwargs(
|
|
|
3838
3906
|
|
|
3839
3907
|
# Step 5.5: validate choices
|
|
3840
3908
|
for f in cmd.flags:
|
|
3841
|
-
if f.
|
|
3842
|
-
|
|
3843
|
-
for val in cli_set[f.name]:
|
|
3844
|
-
if val not in f.choices:
|
|
3845
|
-
choices_str = ", ".join(str(c) for c in f.choices)
|
|
3846
|
-
raise _ParseError(
|
|
3847
|
-
f"--{f.name}: invalid value '{val}', must be one of: {choices_str}"
|
|
3848
|
-
)
|
|
3849
|
-
else:
|
|
3850
|
-
val = cli_set[f.name]
|
|
3851
|
-
if val not in f.choices:
|
|
3852
|
-
choices_str = ", ".join(str(c) for c in f.choices)
|
|
3853
|
-
raise _ParseError(
|
|
3854
|
-
f"--{f.name}: invalid value '{val}', must be one of: {choices_str}"
|
|
3855
|
-
)
|
|
3909
|
+
if f.name in cli_set:
|
|
3910
|
+
_validate_choices(f.name, cli_set[f.name], f.repeatable, f.choices)
|
|
3856
3911
|
|
|
3857
3912
|
# Step 5.6: custom validation
|
|
3858
3913
|
for f in cmd.flags:
|
|
@@ -3863,7 +3918,9 @@ def _validate_and_build_kwargs(
|
|
|
3863
3918
|
f.validate(val)
|
|
3864
3919
|
except ValueError as e:
|
|
3865
3920
|
raise _ParseError(f"--{f.name}: {e}")
|
|
3866
|
-
|
|
3921
|
+
elif cli_set[f.name] is not None:
|
|
3922
|
+
# None means the flag was not passed (an unset mutex flag) --
|
|
3923
|
+
# there is no value to validate.
|
|
3867
3924
|
try:
|
|
3868
3925
|
f.validate(cli_set[f.name])
|
|
3869
3926
|
except ValueError as e:
|
|
@@ -3893,23 +3950,10 @@ def _validate_and_build_kwargs(
|
|
|
3893
3950
|
|
|
3894
3951
|
# Step 6.5: validate arg choices
|
|
3895
3952
|
for a in cmd.args:
|
|
3896
|
-
if a.
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
if v not in a.choices:
|
|
3901
|
-
choices_str = ", ".join(str(c) for c in a.choices)
|
|
3902
|
-
raise _ParseError(
|
|
3903
|
-
f"argument '{a.name}': invalid value '{v}', "
|
|
3904
|
-
f"must be one of: {choices_str}"
|
|
3905
|
-
)
|
|
3906
|
-
else:
|
|
3907
|
-
if val not in a.choices:
|
|
3908
|
-
choices_str = ", ".join(str(c) for c in a.choices)
|
|
3909
|
-
raise _ParseError(
|
|
3910
|
-
f"argument '{a.name}': invalid value '{val}', "
|
|
3911
|
-
f"must be one of: {choices_str}"
|
|
3912
|
-
)
|
|
3953
|
+
if a.name in arg_values:
|
|
3954
|
+
_validate_choices(
|
|
3955
|
+
a.name, arg_values[a.name], a.variadic, a.choices, is_arg=True,
|
|
3956
|
+
)
|
|
3913
3957
|
|
|
3914
3958
|
# Step 7: build kwargs dict (command flags only)
|
|
3915
3959
|
kwargs: dict[str, object] = {}
|
|
@@ -5263,15 +5307,22 @@ def _run_checks(
|
|
|
5263
5307
|
warn with ignore_warnings=True), 1 otherwise.
|
|
5264
5308
|
"""
|
|
5265
5309
|
results: list[tuple[str, CheckResult]] = []
|
|
5310
|
+
# Checks whose dependents should be cascade-skipped: only fail or
|
|
5311
|
+
# cascade-skip qualifies. A warn satisfies the dependency (dependents
|
|
5312
|
+
# still run) regardless of ignore_warnings -- it only affects the exit
|
|
5313
|
+
# code. Explicit skip from an impl is not a failure either.
|
|
5266
5314
|
failed_checks: set[str] = set()
|
|
5267
5315
|
exit_code = 0
|
|
5268
5316
|
|
|
5269
|
-
|
|
5270
|
-
|
|
5271
|
-
|
|
5272
|
-
|
|
5273
|
-
|
|
5274
|
-
|
|
5317
|
+
def record(name: str, result: CheckResult) -> None:
|
|
5318
|
+
nonlocal exit_code
|
|
5319
|
+
if result.status == "fail":
|
|
5320
|
+
failed_checks.add(name)
|
|
5321
|
+
exit_code = 1
|
|
5322
|
+
elif result.status == "warn":
|
|
5323
|
+
if not ignore_warnings:
|
|
5324
|
+
exit_code = 1
|
|
5325
|
+
# "skip" from an impl or adapter: no cascade, no exit code change.
|
|
5275
5326
|
|
|
5276
5327
|
for name in check_names:
|
|
5277
5328
|
cdef = check_defs[name]
|
|
@@ -5298,33 +5349,14 @@ def _run_checks(
|
|
|
5298
5349
|
if cdef.scope and scope_adapter is not None:
|
|
5299
5350
|
adapted = scope_adapter(context, cdef.scope)
|
|
5300
5351
|
if isinstance(adapted, CheckResult):
|
|
5301
|
-
|
|
5302
|
-
|
|
5303
|
-
if result.status == "fail":
|
|
5304
|
-
failed_checks.add(name)
|
|
5305
|
-
exit_code = 1
|
|
5306
|
-
elif result.status == "warn":
|
|
5307
|
-
if not ignore_warnings:
|
|
5308
|
-
failed_checks.add(name)
|
|
5309
|
-
exit_code = 1
|
|
5310
|
-
elif result.status == "skip":
|
|
5311
|
-
pass
|
|
5352
|
+
results.append((name, adapted))
|
|
5353
|
+
record(name, adapted)
|
|
5312
5354
|
continue
|
|
5313
5355
|
check_context = adapted
|
|
5314
5356
|
|
|
5315
5357
|
result = cdef.impl(check_context)
|
|
5316
5358
|
results.append((name, result))
|
|
5317
|
-
|
|
5318
|
-
if result.status == "fail":
|
|
5319
|
-
failed_checks.add(name)
|
|
5320
|
-
exit_code = 1
|
|
5321
|
-
elif result.status == "warn":
|
|
5322
|
-
if not ignore_warnings:
|
|
5323
|
-
failed_checks.add(name)
|
|
5324
|
-
exit_code = 1
|
|
5325
|
-
elif result.status == "skip":
|
|
5326
|
-
# Treat explicit skip from impl as non-failure (no cascade)
|
|
5327
|
-
pass
|
|
5359
|
+
record(name, result)
|
|
5328
5360
|
|
|
5329
5361
|
return results, exit_code
|
|
5330
5362
|
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Registration-time validation of arg defaults.
|
|
2
|
+
|
|
3
|
+
str args must reject non-string defaults, and list (variadic) args must
|
|
4
|
+
validate their default as a list with correctly-typed elements.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
import strictcli
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_arg_str_default_type_validated():
|
|
13
|
+
"""type=str requires a str default."""
|
|
14
|
+
with pytest.raises(ValueError, match="type=str requires a str default"):
|
|
15
|
+
strictcli.Arg(name="x", help="a value", required=False, default=42)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_arg_str_default_rejects_bool():
|
|
19
|
+
"""type=str rejects a bool default."""
|
|
20
|
+
with pytest.raises(ValueError, match="type=str requires a str default"):
|
|
21
|
+
strictcli.Arg(name="x", help="a value", required=False, default=True)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_arg_list_default_must_be_list():
|
|
25
|
+
"""A list arg default must be a list."""
|
|
26
|
+
with pytest.raises(ValueError, match="list arg default must be a list"):
|
|
27
|
+
strictcli.Arg(
|
|
28
|
+
name="items", help="the items", type=list[str],
|
|
29
|
+
variadic=True, required=False, default="nope",
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_arg_list_default_empty_rejected():
|
|
34
|
+
"""An explicit empty list default is redundant and rejected."""
|
|
35
|
+
with pytest.raises(
|
|
36
|
+
ValueError,
|
|
37
|
+
match="explicit empty default is redundant for list args",
|
|
38
|
+
):
|
|
39
|
+
strictcli.Arg(
|
|
40
|
+
name="items", help="the items", type=list[str],
|
|
41
|
+
variadic=True, required=False, default=[],
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_arg_list_default_element_type_validated():
|
|
46
|
+
"""Element types of a list arg default are validated."""
|
|
47
|
+
with pytest.raises(ValueError, match="default element 1 is not of type str"):
|
|
48
|
+
strictcli.Arg(
|
|
49
|
+
name="items", help="the items", type=list[str],
|
|
50
|
+
variadic=True, required=False, default=["a", 2],
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_arg_list_default_int_element_type_validated():
|
|
55
|
+
"""Element types of an int list arg default are validated."""
|
|
56
|
+
with pytest.raises(ValueError, match="default element 0 is not of type int"):
|
|
57
|
+
strictcli.Arg(
|
|
58
|
+
name="nums", help="the numbers", type=list[int],
|
|
59
|
+
variadic=True, required=False, default=["1"],
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_arg_list_default_valid_str():
|
|
64
|
+
"""A well-typed list default is accepted."""
|
|
65
|
+
a = strictcli.Arg(
|
|
66
|
+
name="items", help="the items", type=list[str],
|
|
67
|
+
variadic=True, required=False, default=["a", "b"],
|
|
68
|
+
)
|
|
69
|
+
assert a.default == ["a", "b"]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_arg_list_default_float_coerces_int():
|
|
73
|
+
"""Int elements in a float list default are coerced to float."""
|
|
74
|
+
a = strictcli.Arg(
|
|
75
|
+
name="ratios", help="the ratios", type=list[float],
|
|
76
|
+
variadic=True, required=False, default=[1, 2.5],
|
|
77
|
+
)
|
|
78
|
+
assert a.default == [1.0, 2.5]
|
|
79
|
+
assert isinstance(a.default[0], float)
|
|
@@ -267,7 +267,10 @@ class TestRunChecks:
|
|
|
267
267
|
assert exit_code == 1
|
|
268
268
|
assert results[0][1].status == "warn"
|
|
269
269
|
|
|
270
|
-
def
|
|
270
|
+
def test_warn_dependency_runs_dependent(self, tmp_path, monkeypatch):
|
|
271
|
+
# A warn satisfies a dependency: only fail (or cascade-skip) skips
|
|
272
|
+
# dependents. The warn still makes the run exit non-zero when
|
|
273
|
+
# ignore_warnings=False, but the dependent must run.
|
|
271
274
|
defs = {
|
|
272
275
|
"b": _make_check_def(
|
|
273
276
|
"b",
|
|
@@ -286,7 +289,66 @@ class TestRunChecks:
|
|
|
286
289
|
assert exit_code == 1
|
|
287
290
|
statuses = {name: r.status for name, r in results}
|
|
288
291
|
assert statuses["b"] == "warn"
|
|
289
|
-
assert statuses["a"] == "
|
|
292
|
+
assert statuses["a"] == "pass"
|
|
293
|
+
|
|
294
|
+
def test_warn_dependency_transitive_dependents_run(self, tmp_path, monkeypatch):
|
|
295
|
+
# warn -> dependent -> transitive dependent: the whole chain runs.
|
|
296
|
+
defs = {
|
|
297
|
+
"c": _make_check_def(
|
|
298
|
+
"c",
|
|
299
|
+
impl=lambda ctx: CheckResult(status="warn", message="Warning"),
|
|
300
|
+
),
|
|
301
|
+
"b": _make_check_def(
|
|
302
|
+
"b",
|
|
303
|
+
depends_on=["c"],
|
|
304
|
+
impl=lambda ctx: CheckResult(status="pass", message="B OK"),
|
|
305
|
+
),
|
|
306
|
+
"a": _make_check_def(
|
|
307
|
+
"a",
|
|
308
|
+
depends_on=["b"],
|
|
309
|
+
impl=lambda ctx: CheckResult(status="pass", message="A OK"),
|
|
310
|
+
),
|
|
311
|
+
}
|
|
312
|
+
app = self._make_app_with_checks(defs, tmp_path, monkeypatch)
|
|
313
|
+
ctx = SimpleContext(project_root=tmp_path)
|
|
314
|
+
order = _resolve_check_order(app._check_defs, {"a", "b", "c"})
|
|
315
|
+
results, exit_code = _run_checks(app._check_defs, order, ctx, ignore_warnings=False)
|
|
316
|
+
assert exit_code == 1
|
|
317
|
+
statuses = {name: r.status for name, r in results}
|
|
318
|
+
assert statuses["c"] == "warn"
|
|
319
|
+
assert statuses["b"] == "pass"
|
|
320
|
+
assert statuses["a"] == "pass"
|
|
321
|
+
|
|
322
|
+
def test_warn_from_scope_adapter_runs_dependent(self, tmp_path, monkeypatch):
|
|
323
|
+
# When the scope adapter short-circuits a check with a warn result,
|
|
324
|
+
# the warn must also satisfy dependencies (no cascade-skip).
|
|
325
|
+
defs = {
|
|
326
|
+
"b": _make_check_def(
|
|
327
|
+
"b",
|
|
328
|
+
impl=lambda ctx: CheckResult(status="pass", message="unused"),
|
|
329
|
+
),
|
|
330
|
+
"a": _make_check_def(
|
|
331
|
+
"a",
|
|
332
|
+
depends_on=["b"],
|
|
333
|
+
impl=lambda ctx: CheckResult(status="pass", message="A OK"),
|
|
334
|
+
),
|
|
335
|
+
}
|
|
336
|
+
app = self._make_app_with_checks(defs, tmp_path, monkeypatch)
|
|
337
|
+
# Give "b" a scope so the adapter is consulted for it.
|
|
338
|
+
app._check_defs["b"].scope = "some-scope"
|
|
339
|
+
|
|
340
|
+
def adapter(context, scope):
|
|
341
|
+
return CheckResult(status="warn", message="adapter warning")
|
|
342
|
+
|
|
343
|
+
ctx = SimpleContext(project_root=tmp_path)
|
|
344
|
+
order = _resolve_check_order(app._check_defs, {"a", "b"})
|
|
345
|
+
results, exit_code = _run_checks(
|
|
346
|
+
app._check_defs, order, ctx, ignore_warnings=False, scope_adapter=adapter
|
|
347
|
+
)
|
|
348
|
+
assert exit_code == 1
|
|
349
|
+
statuses = {name: r.status for name, r in results}
|
|
350
|
+
assert statuses["b"] == "warn"
|
|
351
|
+
assert statuses["a"] == "pass"
|
|
290
352
|
|
|
291
353
|
def test_warn_does_not_cascade_when_ignored(self, tmp_path, monkeypatch):
|
|
292
354
|
defs = {
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""Tests for choices/validate exemption of None values.
|
|
2
|
+
|
|
3
|
+
A None value only arises when a flag or arg was not passed (an unset mutex
|
|
4
|
+
flag, or an arg with default=None); a CLI-supplied value is never None.
|
|
5
|
+
Choices validation and custom validators must skip None instead of failing
|
|
6
|
+
with "invalid value 'None'".
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
import strictcli
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# ---------------------------------------------------------------------------
|
|
15
|
+
# Mutex flags: the unset member resolves to None
|
|
16
|
+
# ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _mutex_choices_app():
|
|
20
|
+
mg = strictcli.MutexGroup(
|
|
21
|
+
flags=[
|
|
22
|
+
strictcli.Flag(
|
|
23
|
+
name="format", type=str, help="output format",
|
|
24
|
+
default=None, choices=["text", "json"],
|
|
25
|
+
),
|
|
26
|
+
strictcli.Flag(name="output", type=str, help="output path", default=None),
|
|
27
|
+
],
|
|
28
|
+
)
|
|
29
|
+
app = strictcli.App(name="test", version="1.0.0", help="test app")
|
|
30
|
+
|
|
31
|
+
@app.command("cmd", help="a command", mutex=[mg])
|
|
32
|
+
def cmd(format, output):
|
|
33
|
+
print(f"format={format} output={output}")
|
|
34
|
+
|
|
35
|
+
return app
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_mutex_flag_choices_unset_not_validated():
|
|
39
|
+
"""Choices on an unset mutex flag must not fire when the other member is passed."""
|
|
40
|
+
app = _mutex_choices_app()
|
|
41
|
+
r = app.test(["cmd", "--output", "out.txt"])
|
|
42
|
+
assert r.exit_code == 0
|
|
43
|
+
assert "format=None output=out.txt" in r.stdout
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_mutex_flag_choices_passed_valid():
|
|
47
|
+
"""A valid choice on the mutex flag is still accepted."""
|
|
48
|
+
app = _mutex_choices_app()
|
|
49
|
+
r = app.test(["cmd", "--format", "json"])
|
|
50
|
+
assert r.exit_code == 0
|
|
51
|
+
assert "format=json output=None" in r.stdout
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_mutex_flag_choices_passed_invalid():
|
|
55
|
+
"""An invalid choice on the mutex flag is still rejected."""
|
|
56
|
+
app = _mutex_choices_app()
|
|
57
|
+
r = app.test(["cmd", "--format", "xml"])
|
|
58
|
+
assert r.exit_code == 1
|
|
59
|
+
assert "--format: invalid value 'xml', must be one of: text, json" in r.stderr
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# ---------------------------------------------------------------------------
|
|
63
|
+
# Args: default=None (or no default) resolves to None
|
|
64
|
+
# ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _arg_choices_app(**arg_kwargs):
|
|
68
|
+
app = strictcli.App(name="test", version="1.0.0", help="test app")
|
|
69
|
+
|
|
70
|
+
@app.command(
|
|
71
|
+
"cmd",
|
|
72
|
+
help="a command",
|
|
73
|
+
args=[strictcli.Arg(
|
|
74
|
+
name="env", help="target env", required=False,
|
|
75
|
+
choices=["dev", "staging", "prod"], **arg_kwargs,
|
|
76
|
+
)],
|
|
77
|
+
)
|
|
78
|
+
def cmd(env=None):
|
|
79
|
+
print(f"env={env}")
|
|
80
|
+
|
|
81
|
+
return app
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_arg_none_default_choices_not_passed():
|
|
85
|
+
"""Optional arg with default=None and choices, not passed -> succeeds."""
|
|
86
|
+
app = _arg_choices_app(default=None)
|
|
87
|
+
r = app.test(["cmd"])
|
|
88
|
+
assert r.exit_code == 0
|
|
89
|
+
assert "env=None" in r.stdout
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_arg_optional_no_default_choices_not_passed():
|
|
93
|
+
"""Optional arg with no default and choices, not passed -> succeeds."""
|
|
94
|
+
app = _arg_choices_app()
|
|
95
|
+
r = app.test(["cmd"])
|
|
96
|
+
assert r.exit_code == 0
|
|
97
|
+
assert "env=None" in r.stdout
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_arg_none_default_choices_passed_valid():
|
|
101
|
+
"""A valid choice on the optional arg is still accepted."""
|
|
102
|
+
app = _arg_choices_app(default=None)
|
|
103
|
+
r = app.test(["cmd", "prod"])
|
|
104
|
+
assert r.exit_code == 0
|
|
105
|
+
assert "env=prod" in r.stdout
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def test_arg_none_default_choices_passed_invalid():
|
|
109
|
+
"""An invalid choice on the optional arg is still rejected."""
|
|
110
|
+
app = _arg_choices_app(default=None)
|
|
111
|
+
r = app.test(["cmd", "local"])
|
|
112
|
+
assert r.exit_code == 1
|
|
113
|
+
assert (
|
|
114
|
+
"argument 'env': invalid value 'local', must be one of: dev, staging, prod"
|
|
115
|
+
in r.stderr
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# ---------------------------------------------------------------------------
|
|
120
|
+
# Custom validators: not run for None (not-passed) values
|
|
121
|
+
# ---------------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _name_validator(val):
|
|
125
|
+
if not isinstance(val, str):
|
|
126
|
+
raise ValueError(f"validator received non-string value {val!r}")
|
|
127
|
+
if val == "bad":
|
|
128
|
+
raise ValueError("bad name")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _mutex_validate_app():
|
|
132
|
+
mg = strictcli.MutexGroup(
|
|
133
|
+
flags=[
|
|
134
|
+
strictcli.Flag(
|
|
135
|
+
name="name", type=str, help="a name",
|
|
136
|
+
default=None, validate=_name_validator,
|
|
137
|
+
),
|
|
138
|
+
strictcli.Flag(name="id", type=str, help="an id", default=None),
|
|
139
|
+
],
|
|
140
|
+
)
|
|
141
|
+
app = strictcli.App(name="test", version="1.0.0", help="test app")
|
|
142
|
+
|
|
143
|
+
@app.command("cmd", help="a command", mutex=[mg])
|
|
144
|
+
def cmd(name, id):
|
|
145
|
+
print(f"name={name} id={id}")
|
|
146
|
+
|
|
147
|
+
return app
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def test_mutex_flag_validate_unset_not_called():
|
|
151
|
+
"""A custom validator must not run for an unset mutex flag (value None)."""
|
|
152
|
+
app = _mutex_validate_app()
|
|
153
|
+
r = app.test(["cmd", "--id", "42"])
|
|
154
|
+
assert r.exit_code == 0
|
|
155
|
+
assert "name=None id=42" in r.stdout
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def test_mutex_flag_validate_passed_still_runs():
|
|
159
|
+
"""A passed value is still validated."""
|
|
160
|
+
app = _mutex_validate_app()
|
|
161
|
+
r = app.test(["cmd", "--name", "bad"])
|
|
162
|
+
assert r.exit_code == 1
|
|
163
|
+
assert "--name: bad name" in r.stderr
|
|
@@ -120,8 +120,8 @@ class TestSchemaCommands:
|
|
|
120
120
|
@app.command("deploy", help="Deploy the app")
|
|
121
121
|
@strictcli.flag("target", type=str, help="Deploy target", short="t",
|
|
122
122
|
choices=["prod", "staging"])
|
|
123
|
-
@strictcli.flag("force", type=bool, default=False, help="Force deploy")
|
|
124
|
-
def deploy(target,
|
|
123
|
+
@strictcli.flag("force-deploy", type=bool, default=False, help="Force deploy")
|
|
124
|
+
def deploy(target, force_deploy):
|
|
125
125
|
pass
|
|
126
126
|
|
|
127
127
|
app.test(["--dump-schema"])
|
|
@@ -141,7 +141,7 @@ class TestSchemaCommands:
|
|
|
141
141
|
assert "hidden" not in target_flag # hidden=False is the default, omitted
|
|
142
142
|
|
|
143
143
|
force_flag = cmd["flags"][1]
|
|
144
|
-
assert force_flag["name"] == "force"
|
|
144
|
+
assert force_flag["name"] == "force-deploy"
|
|
145
145
|
assert force_flag["type"] == "bool"
|
|
146
146
|
assert force_flag["negatable"] is True
|
|
147
147
|
assert force_flag["default"] is False
|
|
@@ -428,8 +428,8 @@ class TestSchemaFlagTypes:
|
|
|
428
428
|
app = _make_app()
|
|
429
429
|
|
|
430
430
|
@app.command("cmd", help="A command")
|
|
431
|
-
@strictcli.flag("force", type=bool, default=False, help="Force it", negatable=False)
|
|
432
|
-
def cmd(
|
|
431
|
+
@strictcli.flag("force-it", type=bool, default=False, help="Force it", negatable=False)
|
|
432
|
+
def cmd(force_it):
|
|
433
433
|
pass
|
|
434
434
|
|
|
435
435
|
app.test(["--dump-schema"])
|
|
@@ -848,10 +848,10 @@ class TestSchemaConstraints:
|
|
|
848
848
|
|
|
849
849
|
@app.command("deploy", help="Deploy",
|
|
850
850
|
dependencies=[strictcli.Implies(
|
|
851
|
-
flag="force", implies="yes", value=True)])
|
|
852
|
-
@strictcli.flag("force", type=bool, default=False, help="Force deploy")
|
|
851
|
+
flag="force-deploy", implies="yes", value=True)])
|
|
852
|
+
@strictcli.flag("force-deploy", type=bool, default=False, help="Force deploy")
|
|
853
853
|
@strictcli.flag("yes", type=bool, default=False, help="Skip confirmation")
|
|
854
|
-
def deploy(
|
|
854
|
+
def deploy(force_deploy, yes):
|
|
855
855
|
pass
|
|
856
856
|
|
|
857
857
|
app.test(["--dump-schema"])
|
|
@@ -860,7 +860,7 @@ class TestSchemaConstraints:
|
|
|
860
860
|
assert len(cmd["constraints"]) == 1
|
|
861
861
|
c = cmd["constraints"][0]
|
|
862
862
|
assert c["type"] == "implies"
|
|
863
|
-
assert c["flag"] == "force"
|
|
863
|
+
assert c["flag"] == "force-deploy"
|
|
864
864
|
assert c["implies"] == "yes"
|
|
865
865
|
assert c["value"] is True
|
|
866
866
|
|
|
@@ -897,17 +897,17 @@ class TestSchemaConstraints:
|
|
|
897
897
|
|
|
898
898
|
@app.command("deploy", help="Deploy",
|
|
899
899
|
dependencies=[strictcli.CoRequired(
|
|
900
|
-
flags=["dry-run", "
|
|
900
|
+
flags=["dry-run", "skip-confirm"])])
|
|
901
901
|
@strictcli.flag("dry-run", type=bool, default=False, help="Dry run")
|
|
902
|
-
@strictcli.flag("
|
|
903
|
-
def deploy(dry_run,
|
|
902
|
+
@strictcli.flag("skip-confirm", type=bool, default=False, help="Skip confirmation")
|
|
903
|
+
def deploy(dry_run, skip_confirm):
|
|
904
904
|
pass
|
|
905
905
|
|
|
906
906
|
app.test(["--dump-schema"])
|
|
907
907
|
data = json.loads((tmp_path / ".strictcli" / "schema.json").read_text())
|
|
908
908
|
cmd = data["commands"]["deploy"]
|
|
909
909
|
c = cmd["constraints"][0]
|
|
910
|
-
assert c["flags"] == ["dry-run", "
|
|
910
|
+
assert c["flags"] == ["dry-run", "skip-confirm"]
|
|
911
911
|
|
|
912
912
|
|
|
913
913
|
class TestSchemaTagContracts:
|
|
@@ -928,34 +928,34 @@ class TestSchemaTagContracts:
|
|
|
928
928
|
def test_tag_contracts_serialized(self, tmp_path, monkeypatch):
|
|
929
929
|
monkeypatch.chdir(tmp_path)
|
|
930
930
|
app = _make_app()
|
|
931
|
-
app.tag_contract("dangerous", requires_flag="force")
|
|
931
|
+
app.tag_contract("dangerous", requires_flag="force-deploy")
|
|
932
932
|
|
|
933
933
|
@app.command("deploy", help="Deploy", tags=["dangerous"])
|
|
934
|
-
@strictcli.flag("force", type=bool, default=False, help="Force it")
|
|
935
|
-
def deploy(
|
|
934
|
+
@strictcli.flag("force-deploy", type=bool, default=False, help="Force it")
|
|
935
|
+
def deploy(force_deploy):
|
|
936
936
|
pass
|
|
937
937
|
|
|
938
938
|
app.test(["--dump-schema"])
|
|
939
939
|
data = json.loads((tmp_path / ".strictcli" / "schema.json").read_text())
|
|
940
940
|
assert "tag_contracts" in data
|
|
941
|
-
assert data["tag_contracts"] == {"dangerous": "force"}
|
|
941
|
+
assert data["tag_contracts"] == {"dangerous": "force-deploy"}
|
|
942
942
|
|
|
943
943
|
def test_multiple_tag_contracts(self, tmp_path, monkeypatch):
|
|
944
944
|
monkeypatch.chdir(tmp_path)
|
|
945
945
|
app = _make_app()
|
|
946
|
-
app.tag_contract("dangerous", requires_flag="force")
|
|
946
|
+
app.tag_contract("dangerous", requires_flag="force-deploy")
|
|
947
947
|
app.tag_contract("slow", requires_flag="timeout")
|
|
948
948
|
|
|
949
949
|
@app.command("deploy", help="Deploy", tags=["dangerous", "slow"])
|
|
950
|
-
@strictcli.flag("force", type=bool, default=False, help="Force it")
|
|
950
|
+
@strictcli.flag("force-deploy", type=bool, default=False, help="Force it")
|
|
951
951
|
@strictcli.flag("timeout", type=int, help="Timeout", default=30)
|
|
952
|
-
def deploy(
|
|
952
|
+
def deploy(force_deploy, timeout):
|
|
953
953
|
pass
|
|
954
954
|
|
|
955
955
|
app.test(["--dump-schema"])
|
|
956
956
|
data = json.loads((tmp_path / ".strictcli" / "schema.json").read_text())
|
|
957
957
|
assert data["tag_contracts"] == {
|
|
958
|
-
"dangerous": "force",
|
|
958
|
+
"dangerous": "force-deploy",
|
|
959
959
|
"slow": "timeout",
|
|
960
960
|
}
|
|
961
961
|
|
|
@@ -317,7 +317,7 @@ def test_e2e_kwargs_handler_registration_no_error():
|
|
|
317
317
|
args=[strictcli.Arg(name="name", help="a name")],
|
|
318
318
|
)
|
|
319
319
|
@strictcli.flag("count", type=int, help="a count", default=0)
|
|
320
|
-
@strictcli.flag("force", type=bool, default=False, help="force it")
|
|
320
|
+
@strictcli.flag("force-it", type=bool, default=False, help="force it")
|
|
321
321
|
def cmd_handler(**kwargs):
|
|
322
322
|
return 0
|
|
323
323
|
|
|
@@ -8,8 +8,8 @@ def _make_full_app():
|
|
|
8
8
|
app = strictcli.App(name="myapp", version="2.0.0", help="a great app")
|
|
9
9
|
|
|
10
10
|
@app.command("init", help="initialize the project")
|
|
11
|
-
@strictcli.flag("force", type=bool, default=False, help="overwrite existing files")
|
|
12
|
-
def init(
|
|
11
|
+
@strictcli.flag("force-overwrite", type=bool, default=False, help="overwrite existing files")
|
|
12
|
+
def init(force_overwrite):
|
|
13
13
|
pass
|
|
14
14
|
|
|
15
15
|
@app.command(
|
|
@@ -117,7 +117,7 @@ def test_help_at_command_level():
|
|
|
117
117
|
assert r.exit_code == 0
|
|
118
118
|
assert "init" in r.stdout
|
|
119
119
|
assert "initialize the project" in r.stdout
|
|
120
|
-
assert "--force" in r.stdout
|
|
120
|
+
assert "--force-overwrite" in r.stdout
|
|
121
121
|
|
|
122
122
|
|
|
123
123
|
def test_help_at_group_level():
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|