strictcli 0.9.0__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/changes/0.9.1.jsonl +9 -0
- strictcli-0.10.0/.rlsbl/changes/0.9.1.md +9 -0
- strictcli-0.10.0/.rlsbl/releases/v0.9.0.toml +3 -0
- strictcli-0.10.0/.rlsbl/releases/v0.9.1.toml +3 -0
- strictcli-0.10.0/.rlsbl/version +1 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/CHANGELOG.md +17 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/PKG-INFO +1 -1
- {strictcli-0.9.0 → strictcli-0.10.0}/package-lock.json +2 -2
- {strictcli-0.9.0 → strictcli-0.10.0}/package.json +1 -1
- {strictcli-0.9.0 → strictcli-0.10.0}/pyproject.toml +1 -1
- {strictcli-0.9.0 → strictcli-0.10.0}/strictcli/__init__.py +216 -40
- strictcli-0.10.0/tests/test_at_prefix.py +342 -0
- strictcli-0.10.0/tests/test_config_file_path.py +60 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/tests/test_dump_schema.py +232 -13
- {strictcli-0.9.0 → strictcli-0.10.0}/uv.lock +1 -1
- strictcli-0.9.0/.rlsbl/changes/.validated +0 -1
- strictcli-0.9.0/.rlsbl/version +0 -1
- {strictcli-0.9.0 → strictcli-0.10.0}/.claude/settings.json +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/.github/workflows/ci.yml +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/.github/workflows/publish.yml +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/.gitignore +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/.rlsbl/bases/.claude/settings.json +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/.rlsbl/bases/.github/workflows/ci.yml +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/.rlsbl/bases/.github/workflows/publish.yml +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/.rlsbl/bases/.gitignore +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/.rlsbl/bases/.rlsbl/hooks/post-release.sh +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/.rlsbl/bases/.rlsbl/hooks/pre-checks.sh +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/.rlsbl/bases/.rlsbl/hooks/pre-release.sh +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/.rlsbl/bases/.rlsbl/lint/go.toml +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/.rlsbl/bases/.rlsbl/lint/npm.toml +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/.rlsbl/bases/.rlsbl/lint/python.toml +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/.rlsbl/bases/CHANGELOG.md +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/.rlsbl/bases/CLAUDE.md +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/.rlsbl/bases/LICENSE +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/.rlsbl/changes/0.4.0.jsonl +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/.rlsbl/changes/0.4.0.md +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/.rlsbl/changes/0.4.1.jsonl +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/.rlsbl/changes/0.4.1.md +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/.rlsbl/changes/0.5.0.jsonl +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/.rlsbl/changes/0.5.0.md +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/.rlsbl/changes/0.6.0.jsonl +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/.rlsbl/changes/0.6.0.md +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/.rlsbl/changes/0.6.1.jsonl +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/.rlsbl/changes/0.6.1.md +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/.rlsbl/changes/0.7.0.jsonl +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/.rlsbl/changes/0.7.0.md +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/.rlsbl/changes/0.7.1.jsonl +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/.rlsbl/changes/0.7.1.md +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/.rlsbl/changes/0.8.0.jsonl +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/.rlsbl/changes/0.8.0.md +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/.rlsbl/changes/0.8.1.jsonl +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/.rlsbl/changes/0.8.1.md +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/.rlsbl/changes/0.8.2.jsonl +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/.rlsbl/changes/0.8.2.md +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/.rlsbl/changes/0.8.3.jsonl +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/.rlsbl/changes/0.8.3.md +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/.rlsbl/changes/0.8.4.jsonl +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/.rlsbl/changes/0.8.4.md +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/.rlsbl/changes/0.8.5.jsonl +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/.rlsbl/changes/0.8.5.md +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/.rlsbl/changes/0.8.6.jsonl +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/.rlsbl/changes/0.8.6.md +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/.rlsbl/changes/0.8.7.jsonl +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/.rlsbl/changes/0.8.7.md +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/.rlsbl/changes/0.9.0.jsonl +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/.rlsbl/changes/0.9.0.md +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/.rlsbl/changes/unreleased.jsonl +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/.rlsbl/config.json +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/.rlsbl/hashes.json +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/.rlsbl/hooks/post-release.sh +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/.rlsbl/hooks/pre-checks.sh +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/.rlsbl/hooks/pre-release.sh +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/.rlsbl/lint/go.toml +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/.rlsbl/lint/npm.toml +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/.rlsbl/lint/python.toml +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/.rlsbl/releases/unreleased.toml +0 -0
- /strictcli-0.9.0/.rlsbl/releases/v0.9.0.toml → /strictcli-0.10.0/.rlsbl/releases/v0.10.0.toml +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/.rlsbl/releases/v0.8.5.toml +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/.rlsbl/releases/v0.8.6.toml +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/.rlsbl/releases/v0.8.7.toml +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/.strictcli/schema.json +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/CLAUDE.md +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/LICENSE +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/README.md +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/index.js +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/postinstall.js +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/tests/test_arg_default.py +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/tests/test_auto_version.py +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/tests/test_check_command.py +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/tests/test_check_discovery.py +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/tests/test_check_runner.py +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/tests/test_check_schema.py +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/tests/test_check_types.py +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/tests/test_choices.py +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/tests/test_command_help_suggestion.py +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/tests/test_config.py +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/tests/test_deep_nesting.py +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/tests/test_dependencies.py +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/tests/test_deprecated.py +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/tests/test_e2e.py +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/tests/test_env.py +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/tests/test_exit_codes.py +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/tests/test_float_type.py +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/tests/test_global_flags.py +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/tests/test_help.py +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/tests/test_int_type.py +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/tests/test_mutex.py +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/tests/test_nesting.py +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/tests/test_parser.py +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/tests/test_passthrough.py +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/tests/test_registration.py +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/tests/test_repeatable.py +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/tests/test_tagdsl.py +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/tests/test_tags.py +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/tests/test_toml_loading.py +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/tests/test_validate.py +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/tests/test_variadic.py +0 -0
- {strictcli-0.9.0 → strictcli-0.10.0}/todo/.defer/deferred.md +0 -0
- {strictcli-0.9.0 → 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,9 @@
|
|
|
1
|
+
{"commits":["034d46ccdb83e2e6abdfb27d50553128e8b194da"],"user_facing":true,"description":"**New feature.** Public `config_file_path` property on App.","type":"feature"}
|
|
2
|
+
{"commits":["0eacbbb26047b737ac9ff5a05ccc25f381c6917f"],"user_facing":true,"description":"**Fix.** Config format validation errors now have parity between Python and Go.","type":"fix"}
|
|
3
|
+
{"commits":["a40e7421f7e51240f51c4a8ce9ed72e73af2dd94","3d0745ec3da5d124465ae62a6edda1ba9c586909","e1512089774da17d8e1b50b38af9e00f2b04f401","06ff387b1cf6e867e5cf926ad02c646f4020cd4d","f53fa79fb38d1d7ace07305c06030800fe8173bc"],"user_facing":false}
|
|
4
|
+
{"commits":["052392699e08e4edf48d688f244241c461d7eef7","4c64feea9c5efed933f46950a66985707d287c2c","a3058311994565626d720afb34f95f9230828500","2732196f195b787c8c1785c1d054e31d81be73f6","76e89079ca555173f31e16868a8f0e9da8dd86e9"],"user_facing":false}
|
|
5
|
+
{"commits":["81cfebb6de9674be9fafc0756c19d0dd8a6a29c1","b2317bcce29fdeabb0726e7db6865a917d596d91","0bc0c1f80c5252846a685954052690451676815d","905a378a3360abb1fd00fb009862a2d1875f388c"],"user_facing":false}
|
|
6
|
+
{"commits":["77050e3cdb3cdd396bf83fee87768342903157f7"],"user_facing":false}
|
|
7
|
+
{"commits":["2eb65bb7d36320cf1e398e42fa43a63e2d771042","9276de627df40c14d0a0f7f4cd6f934323d7ee47"],"user_facing":false}
|
|
8
|
+
{"commits":["7ccae37d21414a46c87167f726c2279e61db6b09"],"user_facing":false}
|
|
9
|
+
{"commits":["1179ab585f13a63254148ef67705df81ff07b99d"],"user_facing":false}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.42.0
|
|
@@ -2,6 +2,23 @@
|
|
|
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
|
+
|
|
12
|
+
## 0.9.1
|
|
13
|
+
|
|
14
|
+
### Features
|
|
15
|
+
|
|
16
|
+
- **New feature.** Public `config_file_path` property on App.
|
|
17
|
+
|
|
18
|
+
### Fixes
|
|
19
|
+
|
|
20
|
+
- **Fix.** Config format validation errors now have parity between Python and Go.
|
|
21
|
+
|
|
5
22
|
## 0.9.0
|
|
6
23
|
|
|
7
24
|
### 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")
|
|
@@ -669,6 +713,11 @@ class App:
|
|
|
669
713
|
self._check_defs = {}
|
|
670
714
|
self._checks_enabled = False
|
|
671
715
|
|
|
716
|
+
@property
|
|
717
|
+
def config_file_path(self) -> str:
|
|
718
|
+
"""Return the resolved config file path for this app."""
|
|
719
|
+
return _config_path(self.name, override=self.config_path, config_format=self.config_format)
|
|
720
|
+
|
|
672
721
|
def check(self, name: str):
|
|
673
722
|
"""Decorator to register a check implementation."""
|
|
674
723
|
def decorator(fn):
|
|
@@ -1019,6 +1068,7 @@ class App:
|
|
|
1019
1068
|
raise _DumpSchemaRequested()
|
|
1020
1069
|
|
|
1021
1070
|
# Step 1.5: parse global flags before command routing
|
|
1071
|
+
self._stdin_consumed_by: str | None = None
|
|
1022
1072
|
global_values, remaining = self._parse_global_flags(argv)
|
|
1023
1073
|
self._last_global_values = global_values
|
|
1024
1074
|
|
|
@@ -1087,9 +1137,13 @@ class App:
|
|
|
1087
1137
|
return cmd, rest
|
|
1088
1138
|
|
|
1089
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]
|
|
1090
1143
|
try:
|
|
1091
1144
|
cmd, kwargs, post_global = _parse_command(
|
|
1092
1145
|
cmd, rest, self._global_flags, config_data=self._config_data,
|
|
1146
|
+
stdin_consumed_by=stdin_state,
|
|
1093
1147
|
)
|
|
1094
1148
|
except _ParseError as e:
|
|
1095
1149
|
prefix_parts = [self.name] + path + [cmd.name]
|
|
@@ -1176,7 +1230,10 @@ class App:
|
|
|
1176
1230
|
except ValueError as e:
|
|
1177
1231
|
raise _float_parse_error(f.name, value_part, e)
|
|
1178
1232
|
else:
|
|
1179
|
-
|
|
1233
|
+
resolved, self._stdin_consumed_by = _resolve_at_prefix(
|
|
1234
|
+
f.name, value_part, self._stdin_consumed_by,
|
|
1235
|
+
)
|
|
1236
|
+
_store_value(f, resolved)
|
|
1180
1237
|
i += 1
|
|
1181
1238
|
continue
|
|
1182
1239
|
elif flag_part in negation_lookup:
|
|
@@ -1217,7 +1274,10 @@ class App:
|
|
|
1217
1274
|
except ValueError as e:
|
|
1218
1275
|
raise _float_parse_error(f.name, raw, e)
|
|
1219
1276
|
else:
|
|
1220
|
-
|
|
1277
|
+
resolved, self._stdin_consumed_by = _resolve_at_prefix(
|
|
1278
|
+
f.name, raw, self._stdin_consumed_by,
|
|
1279
|
+
)
|
|
1280
|
+
_store_value(f, resolved)
|
|
1221
1281
|
i += 2
|
|
1222
1282
|
else:
|
|
1223
1283
|
raise _ParseError(f"flag '{tok}' requires a value")
|
|
@@ -1245,7 +1305,10 @@ class App:
|
|
|
1245
1305
|
except ValueError as e:
|
|
1246
1306
|
raise _float_parse_error(f.name, raw, e)
|
|
1247
1307
|
else:
|
|
1248
|
-
|
|
1308
|
+
resolved, self._stdin_consumed_by = _resolve_at_prefix(
|
|
1309
|
+
f.name, raw, self._stdin_consumed_by,
|
|
1310
|
+
)
|
|
1311
|
+
_store_value(f, resolved)
|
|
1249
1312
|
i += 2
|
|
1250
1313
|
else:
|
|
1251
1314
|
raise _ParseError(f"flag '{tok}' requires a value")
|
|
@@ -1292,7 +1355,10 @@ class App:
|
|
|
1292
1355
|
raise _float_parse_error(f.name, env_val, e, env=f.env)
|
|
1293
1356
|
cli_set[f.name] = [coerced] if f.repeatable else coerced
|
|
1294
1357
|
else:
|
|
1295
|
-
|
|
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
|
|
1296
1362
|
|
|
1297
1363
|
# Resolve config values for global flags not set by CLI or env
|
|
1298
1364
|
if self._config_data:
|
|
@@ -1466,12 +1532,18 @@ def _parse_command(
|
|
|
1466
1532
|
tokens: list[str],
|
|
1467
1533
|
global_flags: list[Flag] | None = None,
|
|
1468
1534
|
config_data: dict | None = None,
|
|
1535
|
+
stdin_consumed_by: list[str | None] | None = None,
|
|
1469
1536
|
) -> tuple[Command, dict[str, object], dict[str, object]]:
|
|
1470
1537
|
"""Parse tokens against a resolved command's flags and args.
|
|
1471
1538
|
|
|
1472
1539
|
Returns (cmd, kwargs, global_cli_set) where global_cli_set contains
|
|
1473
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.
|
|
1474
1544
|
"""
|
|
1545
|
+
if stdin_consumed_by is None:
|
|
1546
|
+
stdin_consumed_by = [None]
|
|
1475
1547
|
|
|
1476
1548
|
# Build flag lookup dicts
|
|
1477
1549
|
long_lookup: dict[str, Flag] = {} # --flag-name -> Flag
|
|
@@ -1549,7 +1621,10 @@ def _parse_command(
|
|
|
1549
1621
|
except ValueError as e:
|
|
1550
1622
|
raise _float_parse_error(f.name, value_part, e)
|
|
1551
1623
|
else:
|
|
1552
|
-
|
|
1624
|
+
resolved, stdin_consumed_by[0] = _resolve_at_prefix(
|
|
1625
|
+
f.name, value_part, stdin_consumed_by[0],
|
|
1626
|
+
)
|
|
1627
|
+
_store_value(f, resolved)
|
|
1553
1628
|
elif flag_part in negation_lookup:
|
|
1554
1629
|
raise _ParseError(
|
|
1555
1630
|
f"flag '{flag_part}' is a boolean negation and does not take a value"
|
|
@@ -1588,7 +1663,10 @@ def _parse_command(
|
|
|
1588
1663
|
except ValueError as e:
|
|
1589
1664
|
raise _float_parse_error(f.name, raw, e)
|
|
1590
1665
|
else:
|
|
1591
|
-
|
|
1666
|
+
resolved, stdin_consumed_by[0] = _resolve_at_prefix(
|
|
1667
|
+
f.name, raw, stdin_consumed_by[0],
|
|
1668
|
+
)
|
|
1669
|
+
_store_value(f, resolved)
|
|
1592
1670
|
i += 2
|
|
1593
1671
|
else:
|
|
1594
1672
|
raise _ParseError(f"flag '{tok}' requires a value")
|
|
@@ -1618,7 +1696,10 @@ def _parse_command(
|
|
|
1618
1696
|
except ValueError as e:
|
|
1619
1697
|
raise _float_parse_error(f.name, raw, e)
|
|
1620
1698
|
else:
|
|
1621
|
-
|
|
1699
|
+
resolved, stdin_consumed_by[0] = _resolve_at_prefix(
|
|
1700
|
+
f.name, raw, stdin_consumed_by[0],
|
|
1701
|
+
)
|
|
1702
|
+
_store_value(f, resolved)
|
|
1622
1703
|
i += 2
|
|
1623
1704
|
else:
|
|
1624
1705
|
raise _ParseError(f"flag '{tok}' requires a value")
|
|
@@ -1663,7 +1744,10 @@ def _parse_command(
|
|
|
1663
1744
|
raise _float_parse_error(f.name, env_val, e, env=f.env)
|
|
1664
1745
|
cli_set[f.name] = [coerced] if f.repeatable else coerced
|
|
1665
1746
|
else:
|
|
1666
|
-
|
|
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
|
|
1667
1751
|
|
|
1668
1752
|
# Step 4.2: resolve config values for flags not set by CLI or env
|
|
1669
1753
|
if config_data:
|
|
@@ -2865,66 +2949,158 @@ _TYPE_NAMES = {str: "str", bool: "bool", int: "int", float: "float"}
|
|
|
2865
2949
|
|
|
2866
2950
|
|
|
2867
2951
|
def _serialize_flag(f: Flag) -> dict:
|
|
2868
|
-
"""Serialize a Flag to a JSON-serializable dict.
|
|
2869
|
-
|
|
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 = {
|
|
2870
2958
|
"name": f.name,
|
|
2871
2959
|
"type": _TYPE_NAMES[f.type],
|
|
2872
2960
|
"help": f.help,
|
|
2873
|
-
"short": f.short,
|
|
2874
|
-
"default": f.default,
|
|
2875
|
-
"env": f.env,
|
|
2876
|
-
"choices": f.choices,
|
|
2877
|
-
"repeatable": f.repeatable,
|
|
2878
|
-
"negatable": f.negatable if f.type is bool else None,
|
|
2879
|
-
"hidden": False,
|
|
2880
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
|
|
2881
2977
|
|
|
2882
2978
|
|
|
2883
2979
|
def _serialize_arg(a: Arg) -> dict:
|
|
2884
|
-
"""Serialize an Arg to a JSON-serializable dict.
|
|
2885
|
-
|
|
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 = {
|
|
2886
2986
|
"name": a.name,
|
|
2887
2987
|
"help": a.help,
|
|
2888
|
-
"required": a.required,
|
|
2889
|
-
"variadic": a.variadic,
|
|
2890
2988
|
}
|
|
2989
|
+
if not a.required:
|
|
2990
|
+
d["required"] = a.required
|
|
2991
|
+
if a.variadic:
|
|
2992
|
+
d["variadic"] = a.variadic
|
|
2993
|
+
return d
|
|
2891
2994
|
|
|
2892
2995
|
|
|
2893
2996
|
def _serialize_command(cmd: Command) -> dict:
|
|
2894
|
-
"""Serialize a Command to a JSON-serializable dict.
|
|
2895
|
-
|
|
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 = {
|
|
2896
3003
|
"name": cmd.name,
|
|
2897
3004
|
"help": cmd.help,
|
|
2898
|
-
"flags": [_serialize_flag(f) for f in cmd.flags],
|
|
2899
|
-
"args": [_serialize_arg(a) for a in cmd.args],
|
|
2900
|
-
"passthrough": cmd.passthrough is not None,
|
|
2901
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
|
|
2902
3015
|
|
|
2903
3016
|
|
|
2904
3017
|
def _serialize_group(group: Group) -> dict:
|
|
2905
|
-
"""Serialize a Group to a JSON-serializable dict (recursive).
|
|
2906
|
-
|
|
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 = {
|
|
2907
3024
|
"name": group.name,
|
|
2908
3025
|
"help": group.help,
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
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
|
+
},
|
|
2912
3073
|
}
|
|
2913
3074
|
|
|
2914
3075
|
|
|
2915
3076
|
def _dump_schema(app: App) -> dict:
|
|
2916
|
-
"""Produce a JSON-serializable dict representing the app's command tree.
|
|
2917
|
-
|
|
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(),
|
|
2918
3084
|
"name": app.name,
|
|
2919
3085
|
"version": app.version,
|
|
2920
3086
|
"help": app.help,
|
|
2921
|
-
"env_prefix": app.env_prefix,
|
|
2922
|
-
"config": app.config,
|
|
2923
|
-
"global_flags": [_serialize_flag(f) for f in app._global_flags],
|
|
2924
|
-
"commands": {name: _serialize_command(cmd) for name, cmd in app._commands.items()},
|
|
2925
|
-
"groups": {name: _serialize_group(grp) for name, grp in app._groups.items()},
|
|
2926
|
-
"deprecated": {name: dep.message for name, dep in app._deprecated.items()},
|
|
2927
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
|
|
2928
3104
|
if app._checks_enabled:
|
|
2929
3105
|
schema["checks"] = {
|
|
2930
3106
|
name: {
|