strictcli 0.9.1__tar.gz → 0.10.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.10.0/.rlsbl/changes/.validated +1 -0
- strictcli-0.10.0/.rlsbl/changes/0.10.0.jsonl +6 -0
- strictcli-0.10.0/.rlsbl/changes/0.10.0.md +6 -0
- strictcli-0.10.0/.rlsbl/releases/v0.9.0.toml +3 -0
- strictcli-0.10.0/.rlsbl/version +1 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/CHANGELOG.md +7 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/PKG-INFO +1 -1
- {strictcli-0.9.1 → strictcli-0.10.0}/package-lock.json +2 -2
- {strictcli-0.9.1 → strictcli-0.10.0}/package.json +1 -1
- {strictcli-0.9.1 → strictcli-0.10.0}/pyproject.toml +1 -1
- {strictcli-0.9.1 → strictcli-0.10.0}/strictcli/__init__.py +211 -40
- strictcli-0.10.0/tests/test_at_prefix.py +342 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/tests/test_dump_schema.py +232 -13
- {strictcli-0.9.1 → strictcli-0.10.0}/uv.lock +1 -1
- strictcli-0.9.1/.rlsbl/changes/.validated +0 -1
- strictcli-0.9.1/.rlsbl/version +0 -1
- {strictcli-0.9.1 → strictcli-0.10.0}/.claude/settings.json +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/.github/workflows/ci.yml +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/.github/workflows/publish.yml +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/.gitignore +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/.rlsbl/bases/.claude/settings.json +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/.rlsbl/bases/.github/workflows/ci.yml +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/.rlsbl/bases/.github/workflows/publish.yml +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/.rlsbl/bases/.gitignore +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/.rlsbl/bases/.rlsbl/hooks/post-release.sh +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/.rlsbl/bases/.rlsbl/hooks/pre-checks.sh +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/.rlsbl/bases/.rlsbl/hooks/pre-release.sh +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/.rlsbl/bases/.rlsbl/lint/go.toml +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/.rlsbl/bases/.rlsbl/lint/npm.toml +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/.rlsbl/bases/.rlsbl/lint/python.toml +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/.rlsbl/bases/CHANGELOG.md +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/.rlsbl/bases/CLAUDE.md +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/.rlsbl/bases/LICENSE +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/.rlsbl/changes/0.4.0.jsonl +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/.rlsbl/changes/0.4.0.md +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/.rlsbl/changes/0.4.1.jsonl +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/.rlsbl/changes/0.4.1.md +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/.rlsbl/changes/0.5.0.jsonl +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/.rlsbl/changes/0.5.0.md +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/.rlsbl/changes/0.6.0.jsonl +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/.rlsbl/changes/0.6.0.md +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/.rlsbl/changes/0.6.1.jsonl +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/.rlsbl/changes/0.6.1.md +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/.rlsbl/changes/0.7.0.jsonl +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/.rlsbl/changes/0.7.0.md +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/.rlsbl/changes/0.7.1.jsonl +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/.rlsbl/changes/0.7.1.md +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/.rlsbl/changes/0.8.0.jsonl +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/.rlsbl/changes/0.8.0.md +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/.rlsbl/changes/0.8.1.jsonl +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/.rlsbl/changes/0.8.1.md +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/.rlsbl/changes/0.8.2.jsonl +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/.rlsbl/changes/0.8.2.md +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/.rlsbl/changes/0.8.3.jsonl +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/.rlsbl/changes/0.8.3.md +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/.rlsbl/changes/0.8.4.jsonl +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/.rlsbl/changes/0.8.4.md +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/.rlsbl/changes/0.8.5.jsonl +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/.rlsbl/changes/0.8.5.md +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/.rlsbl/changes/0.8.6.jsonl +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/.rlsbl/changes/0.8.6.md +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/.rlsbl/changes/0.8.7.jsonl +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/.rlsbl/changes/0.8.7.md +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/.rlsbl/changes/0.9.0.jsonl +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/.rlsbl/changes/0.9.0.md +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/.rlsbl/changes/0.9.1.jsonl +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/.rlsbl/changes/0.9.1.md +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/.rlsbl/changes/unreleased.jsonl +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/.rlsbl/config.json +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/.rlsbl/hashes.json +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/.rlsbl/hooks/post-release.sh +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/.rlsbl/hooks/pre-checks.sh +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/.rlsbl/hooks/pre-release.sh +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/.rlsbl/lint/go.toml +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/.rlsbl/lint/npm.toml +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/.rlsbl/lint/python.toml +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/.rlsbl/releases/unreleased.toml +0 -0
- /strictcli-0.9.1/.rlsbl/releases/v0.9.0.toml → /strictcli-0.10.0/.rlsbl/releases/v0.10.0.toml +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/.rlsbl/releases/v0.8.5.toml +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/.rlsbl/releases/v0.8.6.toml +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/.rlsbl/releases/v0.8.7.toml +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/.rlsbl/releases/v0.9.1.toml +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/.strictcli/schema.json +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/CLAUDE.md +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/LICENSE +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/README.md +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/index.js +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/postinstall.js +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/tests/test_arg_default.py +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/tests/test_auto_version.py +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/tests/test_check_command.py +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/tests/test_check_discovery.py +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/tests/test_check_runner.py +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/tests/test_check_schema.py +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/tests/test_check_types.py +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/tests/test_choices.py +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/tests/test_command_help_suggestion.py +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/tests/test_config.py +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/tests/test_config_file_path.py +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/tests/test_deep_nesting.py +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/tests/test_dependencies.py +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/tests/test_deprecated.py +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/tests/test_e2e.py +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/tests/test_env.py +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/tests/test_exit_codes.py +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/tests/test_float_type.py +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/tests/test_global_flags.py +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/tests/test_help.py +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/tests/test_int_type.py +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/tests/test_mutex.py +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/tests/test_nesting.py +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/tests/test_parser.py +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/tests/test_passthrough.py +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/tests/test_registration.py +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/tests/test_repeatable.py +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/tests/test_tagdsl.py +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/tests/test_tags.py +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/tests/test_toml_loading.py +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/tests/test_validate.py +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/tests/test_variadic.py +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/todo/.defer/deferred.md +0 -0
- {strictcli-0.9.1 → strictcli-0.10.0}/todo/.done/original-idea.md +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
241a0b1102d02359dc80c640d8c8a17697f05a9b
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{"commits":["faa8f348f03793084361365f15bd9f8be06046ff"],"user_facing":true,"description":"**New feature.** `--dump-schema` omits fields matching defaults and includes a top-level `defaults` object documenting what missing fields mean.","type":"feature"}
|
|
2
|
+
{"commits":["ef852d540829f5fb248c380b649e8c15e56b849b"],"user_facing":true,"description":"**New feature.** `@`-prefix for string flag values: `@path` reads from file, `@-` reads from stdin, `@@` escapes. 1 MB size limit, trailing whitespace stripped.","type":"feature"}
|
|
3
|
+
{"commits":["4294effc300d83eeba90c3cb90c6429fc443c929","e7e5bc79004fe8c0883fdc5c34a1f36eb8d97e75","6bf56e841883195ea6045386b21f93a4d1c8ee2d"],"user_facing":false}
|
|
4
|
+
{"commits":["05d896e80e8df10ee737f50ae735e99695bbc213","1e354f1681539c39d71e4df7d57090d6b32d3f59","5f8b3d49ab264aa26dbe8c6c1d1f1bebb955d73b"],"user_facing":false}
|
|
5
|
+
{"commits":["33dfbe42738b308dc01df908c8286b48eb97e12f"],"user_facing":false}
|
|
6
|
+
{"commits":["f2b61b9ca7eb86207a0631d01cb672c39dcb89a0"],"user_facing":false}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
## 0.10.0
|
|
2
|
+
|
|
3
|
+
### Features
|
|
4
|
+
|
|
5
|
+
- **New feature.** `--dump-schema` omits fields matching defaults and includes a top-level `defaults` object documenting what missing fields mean.
|
|
6
|
+
- **New feature.** `@`-prefix for string flag values: `@path` reads from file, `@-` reads from stdin, `@@` escapes. 1 MB size limit, trailing whitespace stripped.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.42.0
|
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
# Changelog
|
|
4
4
|
|
|
5
|
+
## 0.10.0
|
|
6
|
+
|
|
7
|
+
### Features
|
|
8
|
+
|
|
9
|
+
- **New feature.** `--dump-schema` omits fields matching defaults and includes a top-level `defaults` object documenting what missing fields mean.
|
|
10
|
+
- **New feature.** `@`-prefix for string flag values: `@path` reads from file, `@-` reads from stdin, `@@` escapes. 1 MB size limit, trailing whitespace stripped.
|
|
11
|
+
|
|
5
12
|
## 0.9.1
|
|
6
13
|
|
|
7
14
|
### Features
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "strictcli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "strictcli",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.10.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.10.0"
|
|
6
6
|
|
|
7
7
|
__all__ = [
|
|
8
8
|
"App", "Flag", "Arg", "Tag", "MutexGroup", "CoRequired", "Requires",
|
|
@@ -316,6 +316,50 @@ def _float_parse_error(
|
|
|
316
316
|
return _ParseError(f"--{flag_name}: expected float, got {raw!r}{suffix}")
|
|
317
317
|
|
|
318
318
|
|
|
319
|
+
_AT_PREFIX_MAX_SIZE = 1024 * 1024 # 1 MB
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _resolve_at_prefix(
|
|
323
|
+
flag_name: str, raw: str, stdin_consumed_by: str | None,
|
|
324
|
+
) -> tuple[str, str | None]:
|
|
325
|
+
"""Resolve @-prefix for string flag values.
|
|
326
|
+
|
|
327
|
+
Returns (resolved_value, updated_stdin_consumed_by).
|
|
328
|
+
"""
|
|
329
|
+
if not raw.startswith("@"):
|
|
330
|
+
return raw, stdin_consumed_by
|
|
331
|
+
if raw.startswith("@@"):
|
|
332
|
+
return raw[1:], stdin_consumed_by
|
|
333
|
+
if raw == "@-":
|
|
334
|
+
if stdin_consumed_by is not None:
|
|
335
|
+
raise _ParseError(
|
|
336
|
+
f"--{flag_name}: stdin (@-) can only be used once per invocation"
|
|
337
|
+
)
|
|
338
|
+
try:
|
|
339
|
+
data = sys.stdin.read(_AT_PREFIX_MAX_SIZE + 1)
|
|
340
|
+
if len(data) > _AT_PREFIX_MAX_SIZE:
|
|
341
|
+
raise _ParseError(f"--{flag_name}: file exceeds 1 MB limit")
|
|
342
|
+
return data.rstrip(), flag_name
|
|
343
|
+
except _ParseError:
|
|
344
|
+
raise
|
|
345
|
+
except Exception:
|
|
346
|
+
raise _ParseError(f"--{flag_name}: cannot read stdin")
|
|
347
|
+
# @path -- read file
|
|
348
|
+
path = raw[1:]
|
|
349
|
+
if not os.path.exists(path):
|
|
350
|
+
raise _ParseError(f"--{flag_name}: file not found: {path}")
|
|
351
|
+
try:
|
|
352
|
+
with open(path, "r") as f:
|
|
353
|
+
data = f.read(_AT_PREFIX_MAX_SIZE + 1)
|
|
354
|
+
if len(data) > _AT_PREFIX_MAX_SIZE:
|
|
355
|
+
raise _ParseError(f"--{flag_name}: file exceeds 1 MB limit")
|
|
356
|
+
return data.rstrip(), stdin_consumed_by
|
|
357
|
+
except _ParseError:
|
|
358
|
+
raise
|
|
359
|
+
except Exception:
|
|
360
|
+
raise _ParseError(f"--{flag_name}: cannot read file: {path}")
|
|
361
|
+
|
|
362
|
+
|
|
319
363
|
def _require_non_empty_str(value: str, field_name: str, class_name: str) -> None:
|
|
320
364
|
if not isinstance(value, str) or not value.strip():
|
|
321
365
|
raise ValueError(f"{class_name}.{field_name} must be a non-empty string")
|
|
@@ -1024,6 +1068,7 @@ class App:
|
|
|
1024
1068
|
raise _DumpSchemaRequested()
|
|
1025
1069
|
|
|
1026
1070
|
# Step 1.5: parse global flags before command routing
|
|
1071
|
+
self._stdin_consumed_by: str | None = None
|
|
1027
1072
|
global_values, remaining = self._parse_global_flags(argv)
|
|
1028
1073
|
self._last_global_values = global_values
|
|
1029
1074
|
|
|
@@ -1092,9 +1137,13 @@ class App:
|
|
|
1092
1137
|
return cmd, rest
|
|
1093
1138
|
|
|
1094
1139
|
# Step 3: parse remaining tokens for the resolved command
|
|
1140
|
+
# Pass stdin_consumed_by as a mutable single-element list so
|
|
1141
|
+
# _parse_command can update the shared state.
|
|
1142
|
+
stdin_state: list[str | None] = [self._stdin_consumed_by]
|
|
1095
1143
|
try:
|
|
1096
1144
|
cmd, kwargs, post_global = _parse_command(
|
|
1097
1145
|
cmd, rest, self._global_flags, config_data=self._config_data,
|
|
1146
|
+
stdin_consumed_by=stdin_state,
|
|
1098
1147
|
)
|
|
1099
1148
|
except _ParseError as e:
|
|
1100
1149
|
prefix_parts = [self.name] + path + [cmd.name]
|
|
@@ -1181,7 +1230,10 @@ class App:
|
|
|
1181
1230
|
except ValueError as e:
|
|
1182
1231
|
raise _float_parse_error(f.name, value_part, e)
|
|
1183
1232
|
else:
|
|
1184
|
-
|
|
1233
|
+
resolved, self._stdin_consumed_by = _resolve_at_prefix(
|
|
1234
|
+
f.name, value_part, self._stdin_consumed_by,
|
|
1235
|
+
)
|
|
1236
|
+
_store_value(f, resolved)
|
|
1185
1237
|
i += 1
|
|
1186
1238
|
continue
|
|
1187
1239
|
elif flag_part in negation_lookup:
|
|
@@ -1222,7 +1274,10 @@ class App:
|
|
|
1222
1274
|
except ValueError as e:
|
|
1223
1275
|
raise _float_parse_error(f.name, raw, e)
|
|
1224
1276
|
else:
|
|
1225
|
-
|
|
1277
|
+
resolved, self._stdin_consumed_by = _resolve_at_prefix(
|
|
1278
|
+
f.name, raw, self._stdin_consumed_by,
|
|
1279
|
+
)
|
|
1280
|
+
_store_value(f, resolved)
|
|
1226
1281
|
i += 2
|
|
1227
1282
|
else:
|
|
1228
1283
|
raise _ParseError(f"flag '{tok}' requires a value")
|
|
@@ -1250,7 +1305,10 @@ class App:
|
|
|
1250
1305
|
except ValueError as e:
|
|
1251
1306
|
raise _float_parse_error(f.name, raw, e)
|
|
1252
1307
|
else:
|
|
1253
|
-
|
|
1308
|
+
resolved, self._stdin_consumed_by = _resolve_at_prefix(
|
|
1309
|
+
f.name, raw, self._stdin_consumed_by,
|
|
1310
|
+
)
|
|
1311
|
+
_store_value(f, resolved)
|
|
1254
1312
|
i += 2
|
|
1255
1313
|
else:
|
|
1256
1314
|
raise _ParseError(f"flag '{tok}' requires a value")
|
|
@@ -1297,7 +1355,10 @@ class App:
|
|
|
1297
1355
|
raise _float_parse_error(f.name, env_val, e, env=f.env)
|
|
1298
1356
|
cli_set[f.name] = [coerced] if f.repeatable else coerced
|
|
1299
1357
|
else:
|
|
1300
|
-
|
|
1358
|
+
resolved, self._stdin_consumed_by = _resolve_at_prefix(
|
|
1359
|
+
f.name, env_val, self._stdin_consumed_by,
|
|
1360
|
+
)
|
|
1361
|
+
cli_set[f.name] = [resolved] if f.repeatable else resolved
|
|
1301
1362
|
|
|
1302
1363
|
# Resolve config values for global flags not set by CLI or env
|
|
1303
1364
|
if self._config_data:
|
|
@@ -1471,12 +1532,18 @@ def _parse_command(
|
|
|
1471
1532
|
tokens: list[str],
|
|
1472
1533
|
global_flags: list[Flag] | None = None,
|
|
1473
1534
|
config_data: dict | None = None,
|
|
1535
|
+
stdin_consumed_by: list[str | None] | None = None,
|
|
1474
1536
|
) -> tuple[Command, dict[str, object], dict[str, object]]:
|
|
1475
1537
|
"""Parse tokens against a resolved command's flags and args.
|
|
1476
1538
|
|
|
1477
1539
|
Returns (cmd, kwargs, global_cli_set) where global_cli_set contains
|
|
1478
1540
|
any global flag values parsed from tokens appearing after the command name.
|
|
1541
|
+
|
|
1542
|
+
stdin_consumed_by is a mutable single-element list tracking which flag
|
|
1543
|
+
has already consumed stdin via @-. Updated in-place.
|
|
1479
1544
|
"""
|
|
1545
|
+
if stdin_consumed_by is None:
|
|
1546
|
+
stdin_consumed_by = [None]
|
|
1480
1547
|
|
|
1481
1548
|
# Build flag lookup dicts
|
|
1482
1549
|
long_lookup: dict[str, Flag] = {} # --flag-name -> Flag
|
|
@@ -1554,7 +1621,10 @@ def _parse_command(
|
|
|
1554
1621
|
except ValueError as e:
|
|
1555
1622
|
raise _float_parse_error(f.name, value_part, e)
|
|
1556
1623
|
else:
|
|
1557
|
-
|
|
1624
|
+
resolved, stdin_consumed_by[0] = _resolve_at_prefix(
|
|
1625
|
+
f.name, value_part, stdin_consumed_by[0],
|
|
1626
|
+
)
|
|
1627
|
+
_store_value(f, resolved)
|
|
1558
1628
|
elif flag_part in negation_lookup:
|
|
1559
1629
|
raise _ParseError(
|
|
1560
1630
|
f"flag '{flag_part}' is a boolean negation and does not take a value"
|
|
@@ -1593,7 +1663,10 @@ def _parse_command(
|
|
|
1593
1663
|
except ValueError as e:
|
|
1594
1664
|
raise _float_parse_error(f.name, raw, e)
|
|
1595
1665
|
else:
|
|
1596
|
-
|
|
1666
|
+
resolved, stdin_consumed_by[0] = _resolve_at_prefix(
|
|
1667
|
+
f.name, raw, stdin_consumed_by[0],
|
|
1668
|
+
)
|
|
1669
|
+
_store_value(f, resolved)
|
|
1597
1670
|
i += 2
|
|
1598
1671
|
else:
|
|
1599
1672
|
raise _ParseError(f"flag '{tok}' requires a value")
|
|
@@ -1623,7 +1696,10 @@ def _parse_command(
|
|
|
1623
1696
|
except ValueError as e:
|
|
1624
1697
|
raise _float_parse_error(f.name, raw, e)
|
|
1625
1698
|
else:
|
|
1626
|
-
|
|
1699
|
+
resolved, stdin_consumed_by[0] = _resolve_at_prefix(
|
|
1700
|
+
f.name, raw, stdin_consumed_by[0],
|
|
1701
|
+
)
|
|
1702
|
+
_store_value(f, resolved)
|
|
1627
1703
|
i += 2
|
|
1628
1704
|
else:
|
|
1629
1705
|
raise _ParseError(f"flag '{tok}' requires a value")
|
|
@@ -1668,7 +1744,10 @@ def _parse_command(
|
|
|
1668
1744
|
raise _float_parse_error(f.name, env_val, e, env=f.env)
|
|
1669
1745
|
cli_set[f.name] = [coerced] if f.repeatable else coerced
|
|
1670
1746
|
else:
|
|
1671
|
-
|
|
1747
|
+
resolved, stdin_consumed_by[0] = _resolve_at_prefix(
|
|
1748
|
+
f.name, env_val, stdin_consumed_by[0],
|
|
1749
|
+
)
|
|
1750
|
+
cli_set[f.name] = [resolved] if f.repeatable else resolved
|
|
1672
1751
|
|
|
1673
1752
|
# Step 4.2: resolve config values for flags not set by CLI or env
|
|
1674
1753
|
if config_data:
|
|
@@ -2870,66 +2949,158 @@ _TYPE_NAMES = {str: "str", bool: "bool", int: "int", float: "float"}
|
|
|
2870
2949
|
|
|
2871
2950
|
|
|
2872
2951
|
def _serialize_flag(f: Flag) -> dict:
|
|
2873
|
-
"""Serialize a Flag to a JSON-serializable dict.
|
|
2874
|
-
|
|
2952
|
+
"""Serialize a Flag to a JSON-serializable dict.
|
|
2953
|
+
|
|
2954
|
+
Identity fields (name, type, help) are always included.
|
|
2955
|
+
Other fields are omitted when they match the schema defaults.
|
|
2956
|
+
"""
|
|
2957
|
+
d: dict = {
|
|
2875
2958
|
"name": f.name,
|
|
2876
2959
|
"type": _TYPE_NAMES[f.type],
|
|
2877
2960
|
"help": f.help,
|
|
2878
|
-
"short": f.short,
|
|
2879
|
-
"default": f.default,
|
|
2880
|
-
"env": f.env,
|
|
2881
|
-
"choices": f.choices,
|
|
2882
|
-
"repeatable": f.repeatable,
|
|
2883
|
-
"negatable": f.negatable if f.type is bool else None,
|
|
2884
|
-
"hidden": False,
|
|
2885
2961
|
}
|
|
2962
|
+
if f.short is not None:
|
|
2963
|
+
d["short"] = f.short
|
|
2964
|
+
if f.default is not None:
|
|
2965
|
+
d["default"] = f.default
|
|
2966
|
+
if f.env is not None:
|
|
2967
|
+
d["env"] = f.env
|
|
2968
|
+
if f.choices is not None:
|
|
2969
|
+
d["choices"] = f.choices
|
|
2970
|
+
if f.repeatable:
|
|
2971
|
+
d["repeatable"] = f.repeatable
|
|
2972
|
+
negatable = f.negatable if f.type is bool else None
|
|
2973
|
+
if negatable is not None:
|
|
2974
|
+
d["negatable"] = negatable
|
|
2975
|
+
# hidden is currently always False, so always omitted
|
|
2976
|
+
return d
|
|
2886
2977
|
|
|
2887
2978
|
|
|
2888
2979
|
def _serialize_arg(a: Arg) -> dict:
|
|
2889
|
-
"""Serialize an Arg to a JSON-serializable dict.
|
|
2890
|
-
|
|
2980
|
+
"""Serialize an Arg to a JSON-serializable dict.
|
|
2981
|
+
|
|
2982
|
+
Identity fields (name, help) are always included.
|
|
2983
|
+
Other fields are omitted when they match the schema defaults.
|
|
2984
|
+
"""
|
|
2985
|
+
d: dict = {
|
|
2891
2986
|
"name": a.name,
|
|
2892
2987
|
"help": a.help,
|
|
2893
|
-
"required": a.required,
|
|
2894
|
-
"variadic": a.variadic,
|
|
2895
2988
|
}
|
|
2989
|
+
if not a.required:
|
|
2990
|
+
d["required"] = a.required
|
|
2991
|
+
if a.variadic:
|
|
2992
|
+
d["variadic"] = a.variadic
|
|
2993
|
+
return d
|
|
2896
2994
|
|
|
2897
2995
|
|
|
2898
2996
|
def _serialize_command(cmd: Command) -> dict:
|
|
2899
|
-
"""Serialize a Command to a JSON-serializable dict.
|
|
2900
|
-
|
|
2997
|
+
"""Serialize a Command to a JSON-serializable dict.
|
|
2998
|
+
|
|
2999
|
+
Identity fields (name, help) are always included.
|
|
3000
|
+
Other fields are omitted when they match the schema defaults.
|
|
3001
|
+
"""
|
|
3002
|
+
d: dict = {
|
|
2901
3003
|
"name": cmd.name,
|
|
2902
3004
|
"help": cmd.help,
|
|
2903
|
-
"flags": [_serialize_flag(f) for f in cmd.flags],
|
|
2904
|
-
"args": [_serialize_arg(a) for a in cmd.args],
|
|
2905
|
-
"passthrough": cmd.passthrough is not None,
|
|
2906
3005
|
}
|
|
3006
|
+
if cmd.passthrough is not None:
|
|
3007
|
+
d["passthrough"] = True
|
|
3008
|
+
flags = [_serialize_flag(f) for f in cmd.flags]
|
|
3009
|
+
if flags:
|
|
3010
|
+
d["flags"] = flags
|
|
3011
|
+
args = [_serialize_arg(a) for a in cmd.args]
|
|
3012
|
+
if args:
|
|
3013
|
+
d["args"] = args
|
|
3014
|
+
return d
|
|
2907
3015
|
|
|
2908
3016
|
|
|
2909
3017
|
def _serialize_group(group: Group) -> dict:
|
|
2910
|
-
"""Serialize a Group to a JSON-serializable dict (recursive).
|
|
2911
|
-
|
|
3018
|
+
"""Serialize a Group to a JSON-serializable dict (recursive).
|
|
3019
|
+
|
|
3020
|
+
Identity fields (name, help) are always included.
|
|
3021
|
+
Other fields are omitted when they match the schema defaults.
|
|
3022
|
+
"""
|
|
3023
|
+
d: dict = {
|
|
2912
3024
|
"name": group.name,
|
|
2913
3025
|
"help": group.help,
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
3026
|
+
}
|
|
3027
|
+
commands = {name: _serialize_command(cmd) for name, cmd in group.commands.items()}
|
|
3028
|
+
if commands:
|
|
3029
|
+
d["commands"] = commands
|
|
3030
|
+
groups = {name: _serialize_group(g) for name, g in group._groups.items()}
|
|
3031
|
+
if groups:
|
|
3032
|
+
d["groups"] = groups
|
|
3033
|
+
deprecated = {name: dep.message for name, dep in group.deprecated.items()}
|
|
3034
|
+
if deprecated:
|
|
3035
|
+
d["deprecated"] = deprecated
|
|
3036
|
+
return d
|
|
3037
|
+
|
|
3038
|
+
|
|
3039
|
+
def _build_schema_defaults() -> dict:
|
|
3040
|
+
"""Return the defaults object documenting what 'missing' means in the schema."""
|
|
3041
|
+
return {
|
|
3042
|
+
"app": {
|
|
3043
|
+
"env_prefix": None,
|
|
3044
|
+
"config": False,
|
|
3045
|
+
"global_flags": [],
|
|
3046
|
+
"commands": {},
|
|
3047
|
+
"groups": {},
|
|
3048
|
+
"deprecated": {},
|
|
3049
|
+
},
|
|
3050
|
+
"flag": {
|
|
3051
|
+
"short": None,
|
|
3052
|
+
"default": None,
|
|
3053
|
+
"env": None,
|
|
3054
|
+
"choices": None,
|
|
3055
|
+
"repeatable": False,
|
|
3056
|
+
"negatable": None,
|
|
3057
|
+
"hidden": False,
|
|
3058
|
+
},
|
|
3059
|
+
"arg": {
|
|
3060
|
+
"required": True,
|
|
3061
|
+
"variadic": False,
|
|
3062
|
+
},
|
|
3063
|
+
"command": {
|
|
3064
|
+
"passthrough": False,
|
|
3065
|
+
"flags": [],
|
|
3066
|
+
"args": [],
|
|
3067
|
+
},
|
|
3068
|
+
"group": {
|
|
3069
|
+
"commands": {},
|
|
3070
|
+
"groups": {},
|
|
3071
|
+
"deprecated": {},
|
|
3072
|
+
},
|
|
2917
3073
|
}
|
|
2918
3074
|
|
|
2919
3075
|
|
|
2920
3076
|
def _dump_schema(app: App) -> dict:
|
|
2921
|
-
"""Produce a JSON-serializable dict representing the app's command tree.
|
|
2922
|
-
|
|
3077
|
+
"""Produce a JSON-serializable dict representing the app's command tree.
|
|
3078
|
+
|
|
3079
|
+
Fields whose values match the schema defaults are omitted.
|
|
3080
|
+
The top-level ``defaults`` key documents what each missing field means.
|
|
3081
|
+
"""
|
|
3082
|
+
schema: dict = {
|
|
3083
|
+
"defaults": _build_schema_defaults(),
|
|
2923
3084
|
"name": app.name,
|
|
2924
3085
|
"version": app.version,
|
|
2925
3086
|
"help": app.help,
|
|
2926
|
-
"env_prefix": app.env_prefix,
|
|
2927
|
-
"config": app.config,
|
|
2928
|
-
"global_flags": [_serialize_flag(f) for f in app._global_flags],
|
|
2929
|
-
"commands": {name: _serialize_command(cmd) for name, cmd in app._commands.items()},
|
|
2930
|
-
"groups": {name: _serialize_group(grp) for name, grp in app._groups.items()},
|
|
2931
|
-
"deprecated": {name: dep.message for name, dep in app._deprecated.items()},
|
|
2932
3087
|
}
|
|
3088
|
+
if app.env_prefix is not None:
|
|
3089
|
+
schema["env_prefix"] = app.env_prefix
|
|
3090
|
+
if app.config:
|
|
3091
|
+
schema["config"] = app.config
|
|
3092
|
+
global_flags = [_serialize_flag(f) for f in app._global_flags]
|
|
3093
|
+
if global_flags:
|
|
3094
|
+
schema["global_flags"] = global_flags
|
|
3095
|
+
commands = {name: _serialize_command(cmd) for name, cmd in app._commands.items()}
|
|
3096
|
+
if commands:
|
|
3097
|
+
schema["commands"] = commands
|
|
3098
|
+
groups = {name: _serialize_group(grp) for name, grp in app._groups.items()}
|
|
3099
|
+
if groups:
|
|
3100
|
+
schema["groups"] = groups
|
|
3101
|
+
deprecated = {name: dep.message for name, dep in app._deprecated.items()}
|
|
3102
|
+
if deprecated:
|
|
3103
|
+
schema["deprecated"] = deprecated
|
|
2933
3104
|
if app._checks_enabled:
|
|
2934
3105
|
schema["checks"] = {
|
|
2935
3106
|
name: {
|