strictcli 0.35.2__tar.gz → 0.35.3__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.35.2 → strictcli-0.35.3}/PKG-INFO +1 -1
- {strictcli-0.35.2 → strictcli-0.35.3}/pyproject.toml +1 -1
- {strictcli-0.35.2 → strictcli-0.35.3}/strictcli/__init__.py +82 -10
- {strictcli-0.35.2 → strictcli-0.35.3}/tests/test_reserved_quartet.py +139 -5
- {strictcli-0.35.2 → strictcli-0.35.3}/uv.lock +1 -1
- {strictcli-0.35.2 → strictcli-0.35.3}/.claude/settings.json +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/.github/workflows/ci.yml +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/.github/workflows/publish.yml +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/.gitignore +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/.rlsbl/bases/.github/workflows/ci.yml +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/.rlsbl/bases/.github/workflows/publish.yml +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/.rlsbl/bases/.gitignore +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/.rlsbl/bases/.rlsbl/lint/python.toml +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/.rlsbl/config.json +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/.rlsbl/lint/python.toml +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/.rlsbl/managed-files.json +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/.rlsbl/version +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/.strictcli/schema.json +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/CLAUDE.md +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/LICENSE +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/README.md +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/scripts/add_effect_classification.py +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/scripts/add_forwarding_declaration.py +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/strictcli/py.typed +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/tests/conftest.py +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/tests/flagship_app.py +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/tests/test_arg_default.py +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/tests/test_arg_default_validation.py +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/tests/test_at_prefix.py +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/tests/test_auto_version.py +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/tests/test_call.py +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/tests/test_check_command.py +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/tests/test_check_discovery.py +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/tests/test_check_provider.py +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/tests/test_check_public_api.py +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/tests/test_check_runner.py +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/tests/test_check_schema.py +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/tests/test_check_types.py +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/tests/test_choices.py +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/tests/test_choices_none.py +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/tests/test_classification.py +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/tests/test_command_help_suggestion.py +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/tests/test_command_tags.py +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/tests/test_compound_types.py +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/tests/test_config.py +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/tests/test_config_fields.py +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/tests/test_config_file_path.py +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/tests/test_config_set_bugs.py +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/tests/test_confirm.py +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/tests/test_connection_env.py +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/tests/test_context.py +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/tests/test_coverage.py +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/tests/test_deep_nesting.py +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/tests/test_dependencies.py +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/tests/test_deprecated.py +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/tests/test_dump_schema.py +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/tests/test_e2e.py +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/tests/test_effects.py +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/tests/test_effects_bypass_check.py +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/tests/test_env.py +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/tests/test_exit_codes.py +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/tests/test_flag_sets.py +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/tests/test_flagship_preview.py +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/tests/test_float_format.py +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/tests/test_float_type.py +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/tests/test_float_vectors.py +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/tests/test_global_flag_conflict_position.py +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/tests/test_global_flags.py +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/tests/test_guard_v2.py +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/tests/test_help.py +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/tests/test_hermetic.py +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/tests/test_infra_env.py +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/tests/test_int_type.py +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/tests/test_invoke.py +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/tests/test_keyword_flags.py +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/tests/test_mcp.py +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/tests/test_mutex.py +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/tests/test_nesting.py +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/tests/test_parser.py +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/tests/test_passthrough.py +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/tests/test_provenance.py +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/tests/test_provenance_phase2.py +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/tests/test_registration.py +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/tests/test_repeatable.py +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/tests/test_reserved_global_flags.py +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/tests/test_tagdsl.py +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/tests/test_toml_loading.py +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/tests/test_tool_export.py +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/tests/test_typed_args.py +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/tests/test_unique.py +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/tests/test_utilities.py +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/tests/test_validate.py +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/tests/test_variadic.py +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/tests/test_visibility.py +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/todo/.defer/deferred.md +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/todo/.done/keyword-collision-in-flag-param-name.md +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/todo/.done/original-idea.md +0 -0
- {strictcli-0.35.2 → strictcli-0.35.3}/todo/.done/public-check-runner-api.md +0 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
__version__ = "0.35.
|
|
5
|
+
__version__ = "0.35.3"
|
|
6
6
|
|
|
7
7
|
__all__ = [
|
|
8
8
|
"App", "Flag", "Arg", "FlagSet", "MutexGroup", "CoRequired", "Requires",
|
|
@@ -6004,17 +6004,20 @@ class App:
|
|
|
6004
6004
|
self._groups["config"] = config_grp
|
|
6005
6005
|
|
|
6006
6006
|
def _pre_scan_reserved_flags(self, argv: list[str]) -> dict:
|
|
6007
|
-
"""
|
|
6007
|
+
"""Pre-scan for the framework-owned reserved flags.
|
|
6008
6008
|
|
|
6009
6009
|
Handles --dump-schema, --mcp, --config, --hermetic and the effects-regime
|
|
6010
6010
|
quartet --dry-run/--yes/--quiet/--verbose.
|
|
6011
6011
|
|
|
6012
|
-
|
|
6013
|
-
|
|
6014
|
-
|
|
6015
|
-
|
|
6016
|
-
|
|
6017
|
-
|
|
6012
|
+
Two regions, two rulesets (contract §7.2, amended):
|
|
6013
|
+
|
|
6014
|
+
- The **pre-command region** (before the first non-flag token, before
|
|
6015
|
+
``--``) recognizes every reserved flag. Known global flags and their
|
|
6016
|
+
values are skipped so that a global-flag value matching a command name
|
|
6017
|
+
does not terminate the region early.
|
|
6018
|
+
- The **command region** recognizes ONLY the quartet, anywhere, exactly
|
|
6019
|
+
like --help/-h. --hermetic/--config/--dump-schema/--mcp stay
|
|
6020
|
+
pre-command-only. See _scan_command_region_quartet.
|
|
6018
6021
|
|
|
6019
6022
|
Returns a dict with keys: dump_schema, serve_mcp, hermetic, config_path,
|
|
6020
6023
|
dry_run, yes, quiet, verbose, err, cleaned_argv.
|
|
@@ -6030,16 +6033,20 @@ class App:
|
|
|
6030
6033
|
|
|
6031
6034
|
result: dict = {}
|
|
6032
6035
|
exclude_indices: set[int] = set()
|
|
6036
|
+
# Index where the command region begins; -1 means "never reached one"
|
|
6037
|
+
# (a bare -- or an unknown flag-like token ended the scan for good).
|
|
6038
|
+
command_region_from = -1
|
|
6033
6039
|
i = 0
|
|
6034
6040
|
while i < len(argv):
|
|
6035
6041
|
tok = argv[i]
|
|
6036
6042
|
|
|
6037
|
-
# -- terminates the
|
|
6043
|
+
# -- terminates the whole scan: everything after it is data
|
|
6038
6044
|
if tok == "--":
|
|
6039
6045
|
break
|
|
6040
6046
|
|
|
6041
|
-
# Non-flag token = command
|
|
6047
|
+
# Non-flag token = the command token: the command region starts here
|
|
6042
6048
|
if not tok.startswith("-") or tok == "-":
|
|
6049
|
+
command_region_from = i
|
|
6043
6050
|
break
|
|
6044
6051
|
|
|
6045
6052
|
# --dump-schema
|
|
@@ -6120,6 +6127,11 @@ class App:
|
|
|
6120
6127
|
# Unknown flag-like token: stop
|
|
6121
6128
|
break
|
|
6122
6129
|
|
|
6130
|
+
if command_region_from >= 0:
|
|
6131
|
+
self._scan_command_region_quartet(
|
|
6132
|
+
argv, command_region_from, result, exclude_indices,
|
|
6133
|
+
)
|
|
6134
|
+
|
|
6123
6135
|
if exclude_indices:
|
|
6124
6136
|
result["cleaned_argv"] = [
|
|
6125
6137
|
tok for j, tok in enumerate(argv) if j not in exclude_indices
|
|
@@ -6129,6 +6141,66 @@ class App:
|
|
|
6129
6141
|
|
|
6130
6142
|
return result
|
|
6131
6143
|
|
|
6144
|
+
def _scan_command_region_quartet(
|
|
6145
|
+
self,
|
|
6146
|
+
argv: list[str],
|
|
6147
|
+
start: int,
|
|
6148
|
+
result: dict,
|
|
6149
|
+
exclude_indices: set[int],
|
|
6150
|
+
) -> None:
|
|
6151
|
+
"""Recognize the reserved quartet in the command region of argv.
|
|
6152
|
+
|
|
6153
|
+
Contract §7.2 (amended 2026-08-04): --dry-run/--yes/--quiet/--verbose are
|
|
6154
|
+
recognized ANYWHERE in argv, exactly like --help/-h, because their
|
|
6155
|
+
applicability is per-command -- requiring them before the command name
|
|
6156
|
+
was backwards. Only the quartet is recognized here; --hermetic,
|
|
6157
|
+
--config, --dump-schema and --mcp remain pre-command-only.
|
|
6158
|
+
|
|
6159
|
+
The scan stops for good at two boundaries:
|
|
6160
|
+
|
|
6161
|
+
- a bare ``--``, after which every token is positional data;
|
|
6162
|
+
- a **passthrough** command's name, after which every token belongs to
|
|
6163
|
+
the child process and is forwarded byte-for-byte. Eating a child's own
|
|
6164
|
+
--verbose would silently change what the child does.
|
|
6165
|
+
|
|
6166
|
+
Routing tokens are walked through the group/command tree so a quartet
|
|
6167
|
+
token may sit anywhere among them. Nothing here raises: routing errors
|
|
6168
|
+
are the real parse's job.
|
|
6169
|
+
"""
|
|
6170
|
+
groups = self._groups
|
|
6171
|
+
commands = self._commands
|
|
6172
|
+
routing_done = False
|
|
6173
|
+
i = start
|
|
6174
|
+
while i < len(argv):
|
|
6175
|
+
tok = argv[i]
|
|
6176
|
+
|
|
6177
|
+
if tok == "--":
|
|
6178
|
+
return
|
|
6179
|
+
|
|
6180
|
+
if tok.startswith("-") and tok != "-":
|
|
6181
|
+
if tok in _RESERVED_QUARTET_TOKENS:
|
|
6182
|
+
result[_RESERVED_QUARTET_TOKENS[tok]] = True
|
|
6183
|
+
exclude_indices.add(i)
|
|
6184
|
+
i += 1
|
|
6185
|
+
continue
|
|
6186
|
+
|
|
6187
|
+
# A non-flag token: a routing token until routing resolves.
|
|
6188
|
+
if not routing_done:
|
|
6189
|
+
grp = groups.get(tok)
|
|
6190
|
+
if grp is not None:
|
|
6191
|
+
groups = grp._groups
|
|
6192
|
+
commands = grp.commands
|
|
6193
|
+
i += 1
|
|
6194
|
+
continue
|
|
6195
|
+
cmd = commands.get(tok)
|
|
6196
|
+
if cmd is not None and cmd.passthrough is not None:
|
|
6197
|
+
return
|
|
6198
|
+
# Resolved a normal command, or hit an unknown/deprecated token
|
|
6199
|
+
# the real parse will report: routing is over either way.
|
|
6200
|
+
routing_done = True
|
|
6201
|
+
|
|
6202
|
+
i += 1
|
|
6203
|
+
|
|
6132
6204
|
def _parse(self, argv: list[str]) -> tuple[Command, dict[str, object] | list[str], dict[str, str]]:
|
|
6133
6205
|
"""Parse argv (without program name) into a resolved Command and kwargs.
|
|
6134
6206
|
|
|
@@ -127,13 +127,107 @@ class TestDelivery:
|
|
|
127
127
|
assert r.exit_code == 0
|
|
128
128
|
assert seen == {"ok": True}
|
|
129
129
|
|
|
130
|
-
|
|
131
|
-
"""
|
|
132
|
-
|
|
130
|
+
@pytest.mark.parametrize("token,key", [
|
|
131
|
+
("--dry-run", "dry_run"),
|
|
132
|
+
("--yes", "yes"),
|
|
133
|
+
("--quiet", "quiet"),
|
|
134
|
+
("--verbose", "verbose"),
|
|
135
|
+
])
|
|
136
|
+
def test_each_flag_is_recognized_after_the_command(self, token, key):
|
|
137
|
+
"""The quartet is recognized anywhere in argv, exactly like --help."""
|
|
138
|
+
r = _quartet_app().test(["run", token])
|
|
139
|
+
assert r.exit_code == 0
|
|
140
|
+
assert r.data[key] is True
|
|
141
|
+
|
|
142
|
+
def test_all_four_together_after_the_command(self):
|
|
143
|
+
r = _quartet_app().test(["run", "--dry-run", "--yes", "--quiet", "--verbose"])
|
|
144
|
+
assert r.exit_code == 0
|
|
145
|
+
assert r.data == {"dry_run": True, "yes": True, "quiet": True, "verbose": True}
|
|
146
|
+
|
|
147
|
+
def test_mixed_positions_are_unioned(self):
|
|
148
|
+
r = _quartet_app().test(["--dry-run", "run", "--verbose"])
|
|
149
|
+
assert r.exit_code == 0
|
|
150
|
+
assert r.data["dry_run"] is True
|
|
151
|
+
assert r.data["verbose"] is True
|
|
152
|
+
|
|
153
|
+
def test_recognized_after_a_nested_group_subcommand(self):
|
|
154
|
+
app = strictcli.App(name="app", version="1.0.0", help="app")
|
|
155
|
+
outer = app.group("outer", help="outer")
|
|
156
|
+
inner = outer.group("inner", help="inner")
|
|
157
|
+
|
|
158
|
+
@inner.command("run", effect="read_only", help="run")
|
|
159
|
+
def _run(ctx):
|
|
160
|
+
return strictcli.outcome(data={"dry_run": ctx.dry_run})
|
|
161
|
+
|
|
162
|
+
r = app.test(["outer", "inner", "run", "--dry-run"])
|
|
163
|
+
assert r.exit_code == 0
|
|
164
|
+
assert r.data == {"dry_run": True}
|
|
165
|
+
|
|
166
|
+
def test_recognized_between_a_group_and_its_subcommand(self):
|
|
167
|
+
app = strictcli.App(name="app", version="1.0.0", help="app")
|
|
168
|
+
grp = app.group("grp", help="grp")
|
|
169
|
+
|
|
170
|
+
@grp.command("run", effect="read_only", help="run")
|
|
171
|
+
def _run(ctx):
|
|
172
|
+
return strictcli.outcome(data={"dry_run": ctx.dry_run})
|
|
173
|
+
|
|
174
|
+
r = app.test(["grp", "--dry-run", "run"])
|
|
175
|
+
assert r.exit_code == 0
|
|
176
|
+
assert r.data == {"dry_run": True}
|
|
177
|
+
|
|
178
|
+
def test_stripped_from_argv_after_the_command(self):
|
|
179
|
+
"""The quartet never reaches the command parser as an argument."""
|
|
180
|
+
app = strictcli.App(name="app", version="1.0.0", help="app")
|
|
181
|
+
|
|
182
|
+
@app.command("run", effect="read_only", help="run",
|
|
183
|
+
args=[strictcli.Arg(name="name", help="a positional")])
|
|
184
|
+
def _run(ctx, name):
|
|
185
|
+
return strictcli.outcome(data={"name": name, "quiet": ctx.quiet})
|
|
186
|
+
|
|
187
|
+
r = app.test(["run", "--quiet", "value"])
|
|
188
|
+
assert r.exit_code == 0
|
|
189
|
+
assert r.data == {"name": "value", "quiet": True}
|
|
190
|
+
|
|
191
|
+
def test_a_token_after_double_dash_is_data(self):
|
|
192
|
+
"""A bare -- ends the scan: what follows is positional data."""
|
|
193
|
+
app = strictcli.App(name="app", version="1.0.0", help="app")
|
|
194
|
+
|
|
195
|
+
@app.command("run", effect="read_only", help="run",
|
|
196
|
+
args=[strictcli.Arg(name="rest", help="trailing args",
|
|
197
|
+
variadic=True)])
|
|
198
|
+
def _run(ctx, rest):
|
|
199
|
+
return strictcli.outcome(data={"rest": rest, "dry_run": ctx.dry_run})
|
|
200
|
+
|
|
201
|
+
r = app.test(["run", "--", "--dry-run"])
|
|
202
|
+
assert r.exit_code == 0
|
|
203
|
+
assert r.data == {"rest": ["--dry-run"], "dry_run": False}
|
|
204
|
+
|
|
205
|
+
def test_hermetic_stays_pre_command_only(self):
|
|
206
|
+
"""Only the quartet moved; --hermetic is still pre-command-only."""
|
|
133
207
|
app = _quartet_app()
|
|
134
|
-
r = app.test(["run", "--
|
|
208
|
+
r = app.test(["run", "--hermetic"])
|
|
135
209
|
assert r.exit_code == 1
|
|
136
|
-
assert "unknown flag '--
|
|
210
|
+
assert "unknown flag '--hermetic'" in r.stderr
|
|
211
|
+
|
|
212
|
+
def test_read_only_accepts_a_post_command_dry_run(self):
|
|
213
|
+
app = _quartet_app()
|
|
214
|
+
r = app.test(["run", "--dry-run"])
|
|
215
|
+
assert r.exit_code == 0
|
|
216
|
+
assert "DRY RUN — no changes were made. Would do:" in r.stdout
|
|
217
|
+
|
|
218
|
+
def test_read_only_still_rejects_a_mutating_effect_post_command(self):
|
|
219
|
+
"""Per-command applicability is unchanged, wherever --dry-run appeared."""
|
|
220
|
+
app = strictcli.App(name="app", version="1.0.0", help="app")
|
|
221
|
+
|
|
222
|
+
@app.command("look", effect="read_only", help="look")
|
|
223
|
+
def _look(ctx):
|
|
224
|
+
ctx.effects.mkdir("d")
|
|
225
|
+
return 0
|
|
226
|
+
|
|
227
|
+
with pytest.raises(ValueError) as exc:
|
|
228
|
+
app.test(["look", "--dry-run"])
|
|
229
|
+
assert ('command "look" is classified read_only; '
|
|
230
|
+
"effects.mkdir is a mutating operation") in str(exc.value)
|
|
137
231
|
|
|
138
232
|
def test_passthrough_args_stay_opaque(self):
|
|
139
233
|
app = strictcli.App(name="app", version="1.0.0", help="app")
|
|
@@ -152,6 +246,46 @@ class TestDelivery:
|
|
|
152
246
|
assert r.exit_code == 0
|
|
153
247
|
assert seen["args"] == ["--quiet", "--verbose"]
|
|
154
248
|
|
|
249
|
+
def test_passthrough_under_a_group_stays_opaque(self):
|
|
250
|
+
app = strictcli.App(name="app", version="1.0.0", help="app")
|
|
251
|
+
seen = {}
|
|
252
|
+
|
|
253
|
+
def _pt(ctx, name, args, globals):
|
|
254
|
+
seen["args"] = args
|
|
255
|
+
seen["verbose"] = ctx.verbose
|
|
256
|
+
return 0
|
|
257
|
+
|
|
258
|
+
grp = app.group("grp", help="grp")
|
|
259
|
+
|
|
260
|
+
@grp.command("exec", effect="read_only", help="exec",
|
|
261
|
+
passthrough=strictcli.Passthrough(handler=_pt))
|
|
262
|
+
def _exec(ctx, **kw):
|
|
263
|
+
return 0
|
|
264
|
+
|
|
265
|
+
r = app.test(["grp", "exec", "--verbose", "child"])
|
|
266
|
+
assert r.exit_code == 0
|
|
267
|
+
assert seen == {"args": ["--verbose", "child"], "verbose": False}
|
|
268
|
+
|
|
269
|
+
def test_pre_command_position_is_the_passthrough_escape_hatch(self):
|
|
270
|
+
"""A pre-command quartet token reaches the Context AND leaves the
|
|
271
|
+
child's identically-spelled argument untouched."""
|
|
272
|
+
app = strictcli.App(name="app", version="1.0.0", help="app")
|
|
273
|
+
seen = {}
|
|
274
|
+
|
|
275
|
+
def _pt(ctx, name, args, globals):
|
|
276
|
+
seen["args"] = args
|
|
277
|
+
seen["verbose"] = ctx.verbose
|
|
278
|
+
return 0
|
|
279
|
+
|
|
280
|
+
@app.command("exec", effect="read_only", help="exec",
|
|
281
|
+
passthrough=strictcli.Passthrough(handler=_pt))
|
|
282
|
+
def _exec(ctx, **kw):
|
|
283
|
+
return 0
|
|
284
|
+
|
|
285
|
+
r = app.test(["--verbose", "exec", "--verbose", "child"])
|
|
286
|
+
assert r.exit_code == 0
|
|
287
|
+
assert seen == {"args": ["--verbose", "child"], "verbose": True}
|
|
288
|
+
|
|
155
289
|
|
|
156
290
|
class TestGating:
|
|
157
291
|
"""The --quiet/--verbose gating table (quiet dominates verbose)."""
|
|
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
|
|
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
|