strictcli 0.42.0__tar.gz → 0.43.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.42.0 → strictcli-0.43.0}/PKG-INFO +6 -6
- {strictcli-0.42.0 → strictcli-0.43.0}/README.md +2 -2
- {strictcli-0.42.0 → strictcli-0.43.0}/pyproject.toml +4 -4
- {strictcli-0.42.0 → strictcli-0.43.0}/strictcli/__init__.py +389 -33
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_effects_bypass_check.py +67 -1
- strictcli-0.43.0/tests/test_retired_choices.py +388 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/uv.lock +1 -1
- {strictcli-0.42.0 → strictcli-0.43.0}/.claude/settings.json +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/.github/workflows/ci.yml +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/.github/workflows/publish.yml +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/.gitignore +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/.rlsbl/config.json +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/.rlsbl/lint/python.toml +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/.rlsbl/managed-files.json +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/CLAUDE.md +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/LICENSE +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/scripts/add_effect_classification.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/scripts/add_forwarding_declaration.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/scripts/migrate_presence.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/strictcli/py.typed +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/conftest.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/flagship_app.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_arg_decorator_order.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_arg_default.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_arg_default_validation.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_argv_value_order.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_at_prefix.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_auto_version.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_call.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_check_command.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_check_discovery.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_check_provider.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_check_public_api.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_check_runner.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_check_schema.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_check_types.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_choice_records.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_choices.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_choices_none.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_claimed_rendering.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_classification.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_command_help_suggestion.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_command_tags.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_compound_types.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_config.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_config_fields.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_config_file_path.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_config_set_bugs.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_confirm.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_connection_env.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_constraints.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_context.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_coverage.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_deep_nesting.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_deprecated.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_dry_run_unsupported.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_dump_schema.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_e2e.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_effects.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_env.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_exit_codes.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_flag_sets.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_flagship_preview.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_flat_pre_typed.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_float_format.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_float_type.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_float_vectors.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_global_flag_conflict_position.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_global_flags.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_guard_v2.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_help.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_hermetic.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_infra_env.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_int_type.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_invoke.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_keyword_flags.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_machine_mode.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_mcp.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_member_spelling.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_nesting.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_owns_stdout.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_parser.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_passthrough.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_payload_schema.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_presence.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_provenance.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_provenance_phase2.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_record_pre_typed.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_registration.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_repeatable.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_reserved_global_flags.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_reserved_quartet.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_selectors.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_tagdsl.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_toml_loading.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_tool_export.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_trace_store.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_typed_args.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_ulid_vectors.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_unique.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_update.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_utilities.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_validate.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_value_sweep_order.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_variadic.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/tests/test_visibility.py +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/todo/.defer/deferred.md +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/todo/.done/keyword-collision-in-flag-param-name.md +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/todo/.done/original-idea.md +0 -0
- {strictcli-0.42.0 → strictcli-0.43.0}/todo/.done/public-check-runner-api.md +0 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: strictcli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.43.0
|
|
4
4
|
Summary: A CLI framework for the Era of Agents: nothing is inferred, everything is declared. First-class support for Go, Python, and TypeScript (Python implementation)
|
|
5
5
|
Project-URL: Homepage, https://smmh.dev/strictcli/
|
|
6
6
|
Project-URL: Documentation, https://smmh.dev/strictcli/
|
|
7
|
-
Project-URL: Repository, https://github.com/
|
|
8
|
-
Project-URL: Issues, https://github.com/
|
|
9
|
-
Project-URL: Changelog, https://github.com/
|
|
7
|
+
Project-URL: Repository, https://github.com/stricttools/strictcli
|
|
8
|
+
Project-URL: Issues, https://github.com/stricttools/strictcli/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/stricttools/strictcli/blob/main/CHANGELOG.md
|
|
10
10
|
Author-email: "S. M. Hosseini" <m.hosseini@veliu.com>
|
|
11
11
|
License-Expression: MIT
|
|
12
12
|
License-File: LICENSE
|
|
@@ -657,8 +657,8 @@ paths have no TTY contract, so a consequential command is dispatched directly.
|
|
|
657
657
|
|
|
658
658
|
## See also
|
|
659
659
|
|
|
660
|
-
- [strictcli monorepo](https://github.com/
|
|
661
|
-
- [Go implementation](https://github.com/
|
|
660
|
+
- [strictcli monorepo](https://github.com/stricttools/strictcli) -- conformance tests, Go implementation, and project documentation
|
|
661
|
+
- [Go implementation](https://github.com/stricttools/strictcli/tree/main/go) -- same semantics, functional options API
|
|
662
662
|
|
|
663
663
|
## License
|
|
664
664
|
|
|
@@ -633,8 +633,8 @@ paths have no TTY contract, so a consequential command is dispatched directly.
|
|
|
633
633
|
|
|
634
634
|
## See also
|
|
635
635
|
|
|
636
|
-
- [strictcli monorepo](https://github.com/
|
|
637
|
-
- [Go implementation](https://github.com/
|
|
636
|
+
- [strictcli monorepo](https://github.com/stricttools/strictcli) -- conformance tests, Go implementation, and project documentation
|
|
637
|
+
- [Go implementation](https://github.com/stricttools/strictcli/tree/main/go) -- same semantics, functional options API
|
|
638
638
|
|
|
639
639
|
## License
|
|
640
640
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "strictcli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.43.0"
|
|
8
8
|
description = "A CLI framework for the Era of Agents: nothing is inferred, everything is declared. First-class support for Go, Python, and TypeScript (Python implementation)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -42,6 +42,6 @@ testpaths = ["tests"]
|
|
|
42
42
|
[project.urls]
|
|
43
43
|
Homepage = "https://smmh.dev/strictcli/"
|
|
44
44
|
Documentation = "https://smmh.dev/strictcli/"
|
|
45
|
-
Repository = "https://github.com/
|
|
46
|
-
Issues = "https://github.com/
|
|
47
|
-
Changelog = "https://github.com/
|
|
45
|
+
Repository = "https://github.com/stricttools/strictcli"
|
|
46
|
+
Issues = "https://github.com/stricttools/strictcli/issues"
|
|
47
|
+
Changelog = "https://github.com/stricttools/strictcli/blob/main/CHANGELOG.md"
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
__version__ = "0.
|
|
5
|
+
__version__ = "0.43.0"
|
|
6
6
|
|
|
7
7
|
__all__ = [
|
|
8
8
|
"App", "Flag", "Arg", "FlagSet",
|
|
@@ -15,6 +15,8 @@ __all__ = [
|
|
|
15
15
|
# The scoped-selector construct (contract §24)
|
|
16
16
|
"Choice", "choice", "choice_flag", "sub_flag", "sub_choice_flag",
|
|
17
17
|
"member_value", "provided",
|
|
18
|
+
# Retired choices: the value-level twin of the deprecated-command construct
|
|
19
|
+
"RetiredChoice",
|
|
18
20
|
"Grant", "EffectFailed", "Unsettled", "Completed", "Spawned", "Response",
|
|
19
21
|
"PROC_MUTATE", "PROC_SPAWN", "FILE_WRITE", "NET_MUTATE",
|
|
20
22
|
"flag", "arg",
|
|
@@ -2796,36 +2798,89 @@ def _open_is_write_mode(node) -> bool:
|
|
|
2796
2798
|
return any(ch in mode for ch in ("w", "a", "x", "+"))
|
|
2797
2799
|
|
|
2798
2800
|
|
|
2801
|
+
class _BypassRootNotAWorkTree(Exception):
|
|
2802
|
+
"""The project root handed to the lint is not inside a git work tree."""
|
|
2803
|
+
|
|
2804
|
+
|
|
2805
|
+
def _msg_effects_bypass_not_a_work_tree(root: str) -> str:
|
|
2806
|
+
"""Message template: the lint's input rule, refused at its one door."""
|
|
2807
|
+
return (
|
|
2808
|
+
f"effects-bypass: project root '{root}' is not a git work tree; "
|
|
2809
|
+
f"the check reads only repository-owned files"
|
|
2810
|
+
)
|
|
2811
|
+
|
|
2812
|
+
|
|
2813
|
+
def _bypass_path_is_skipped(rel: str) -> bool:
|
|
2814
|
+
"""True when a repository-owned path sits under a skipped directory.
|
|
2815
|
+
|
|
2816
|
+
git lists tracked files wherever they are, including the directories the
|
|
2817
|
+
analyser never reads -- a vendored tree, a committed ``testdata`` or
|
|
2818
|
+
``build`` directory, a dot-directory's contents. The skip list therefore
|
|
2819
|
+
still applies, now as a filter over git's answer rather than as a walk
|
|
2820
|
+
pruner.
|
|
2821
|
+
"""
|
|
2822
|
+
# git reports forward slashes on every platform, so the split is on "/"
|
|
2823
|
+
# rather than os.sep.
|
|
2824
|
+
return any(
|
|
2825
|
+
part in _BYPASS_SKIP_DIRS or part.startswith(".")
|
|
2826
|
+
for part in rel.split("/")[:-1]
|
|
2827
|
+
)
|
|
2828
|
+
|
|
2829
|
+
|
|
2830
|
+
def _bypass_repo_files(root: Path) -> list[str]:
|
|
2831
|
+
"""The repository-owned Python files under ``root``, relative to it.
|
|
2832
|
+
|
|
2833
|
+
The input set is what git reports: tracked files plus untracked files
|
|
2834
|
+
``.gitignore`` does not exclude. A release-blocking check reads only inputs
|
|
2835
|
+
the repository owns, so a gitignored scratch file -- present on one machine
|
|
2836
|
+
and absent from a fresh clone -- can never decide the verdict. There is no
|
|
2837
|
+
filesystem-walk fallback: a root outside a work tree is refused.
|
|
2838
|
+
"""
|
|
2839
|
+
try:
|
|
2840
|
+
proc = subprocess.run(
|
|
2841
|
+
["git", "ls-files", "--cached", "--others", "--exclude-standard", "-z"],
|
|
2842
|
+
cwd=str(root),
|
|
2843
|
+
capture_output=True,
|
|
2844
|
+
check=False,
|
|
2845
|
+
)
|
|
2846
|
+
except OSError as exc:
|
|
2847
|
+
raise _BypassRootNotAWorkTree(
|
|
2848
|
+
_msg_effects_bypass_not_a_work_tree(str(root))
|
|
2849
|
+
) from exc
|
|
2850
|
+
if proc.returncode != 0:
|
|
2851
|
+
raise _BypassRootNotAWorkTree(
|
|
2852
|
+
_msg_effects_bypass_not_a_work_tree(str(root))
|
|
2853
|
+
)
|
|
2854
|
+
listed = proc.stdout.decode("utf-8", "replace").split("\0")
|
|
2855
|
+
return sorted({
|
|
2856
|
+
rel for rel in listed
|
|
2857
|
+
if rel.endswith(".py") and not _bypass_path_is_skipped(rel)
|
|
2858
|
+
})
|
|
2859
|
+
|
|
2860
|
+
|
|
2799
2861
|
def _scan_effects_bypasses(root: Path) -> list[tuple]:
|
|
2800
2862
|
"""Find direct effect calls REACHABLE FROM a registered command handler.
|
|
2801
2863
|
|
|
2802
2864
|
Returns ``(relative_path, lineno, function_name, target)`` tuples, in file
|
|
2803
|
-
then line order. See :func:`_bypass_reachable_functions` for the scope rule
|
|
2865
|
+
then line order. See :func:`_bypass_reachable_functions` for the scope rule
|
|
2866
|
+
and :func:`_bypass_repo_files` for the input rule. Raises
|
|
2867
|
+
:class:`_BypassRootNotAWorkTree` when the root is not inside a git work
|
|
2868
|
+
tree.
|
|
2804
2869
|
"""
|
|
2805
2870
|
findings: list[tuple] = []
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
except (OSError, SyntaxError, UnicodeDecodeError, ValueError):
|
|
2820
|
-
# A file the analyser cannot read is not evidence of a bypass.
|
|
2821
|
-
continue
|
|
2822
|
-
rel = os.path.relpath(path, root)
|
|
2823
|
-
reachable = _bypass_reachable_functions(tree)
|
|
2824
|
-
if not reachable:
|
|
2825
|
-
continue
|
|
2826
|
-
_bypass_walk(tree, [], reachable, findings, rel,
|
|
2827
|
-
_bypass_effects_aliases(tree),
|
|
2828
|
-
_bypass_import_bindings(tree))
|
|
2871
|
+
for rel in _bypass_repo_files(root):
|
|
2872
|
+
path = os.path.join(str(root), rel)
|
|
2873
|
+
try:
|
|
2874
|
+
tree = ast.parse(Path(path).read_text(encoding="utf-8"), filename=path)
|
|
2875
|
+
except (OSError, SyntaxError, UnicodeDecodeError, ValueError):
|
|
2876
|
+
# A file the analyser cannot read is not evidence of a bypass.
|
|
2877
|
+
continue
|
|
2878
|
+
reachable = _bypass_reachable_functions(tree)
|
|
2879
|
+
if not reachable:
|
|
2880
|
+
continue
|
|
2881
|
+
_bypass_walk(tree, [], reachable, findings, rel,
|
|
2882
|
+
_bypass_effects_aliases(tree),
|
|
2883
|
+
_bypass_import_bindings(tree))
|
|
2829
2884
|
return findings
|
|
2830
2885
|
|
|
2831
2886
|
|
|
@@ -4604,6 +4659,7 @@ _SCOPE_FIELD_KEY = "strictcli_scope"
|
|
|
4604
4659
|
_RECORD_SOURCES_ATTR = "__strictcli_sources__"
|
|
4605
4660
|
|
|
4606
4661
|
_RECORD_SPELLING = "Choice(<value>, help=...)"
|
|
4662
|
+
_RETIRED_RECORD_SPELLING = 'RetiredChoice(<value>, message="<message>")'
|
|
4607
4663
|
_SELECTOR_SPELLING = "choice_flag(...)"
|
|
4608
4664
|
_MEMBER_SELECTOR_SPELLING = 'choice_flag(..., elect_by="member-flags")'
|
|
4609
4665
|
# The payload-carrying member's own declaration, which is where its short goes:
|
|
@@ -4636,6 +4692,223 @@ class Choice:
|
|
|
4636
4692
|
_require_non_empty_str(self.help, "help", "Choice")
|
|
4637
4693
|
|
|
4638
4694
|
|
|
4695
|
+
@dataclass(frozen=True)
|
|
4696
|
+
class RetiredChoice:
|
|
4697
|
+
"""One retired spelling of a value flag or positional arg.
|
|
4698
|
+
|
|
4699
|
+
The value-level twin of ``app.deprecate``: a value the declaration used to
|
|
4700
|
+
accept, plus the message that names its replacement. A retired spelling is
|
|
4701
|
+
refused at parse time ahead of the invalid-value check, and it is NOT a
|
|
4702
|
+
choice -- help never lists it, and the published ``value_schema`` enum and
|
|
4703
|
+
the MCP projection derived from it carry the live set only.
|
|
4704
|
+
|
|
4705
|
+
The message is mandatory and non-empty, like every other message the
|
|
4706
|
+
framework prints on a declaration's behalf. There is no exhaustiveness
|
|
4707
|
+
story to tell: a handler never receives a retired value, so no closed set
|
|
4708
|
+
reaches a delivery site.
|
|
4709
|
+
"""
|
|
4710
|
+
|
|
4711
|
+
value: object
|
|
4712
|
+
message: str = field(kw_only=True)
|
|
4713
|
+
|
|
4714
|
+
|
|
4715
|
+
def _raise_retired_choices_entry_not_record(surface: str, name: str, index: int):
|
|
4716
|
+
"""Message template: a bare ``retired_choices=`` entry.
|
|
4717
|
+
|
|
4718
|
+
Python-only. Go's variadic ``RetiredChoices(...RetiredChoiceValue)`` and
|
|
4719
|
+
TypeScript's record type refuse a bare value at compile time, so neither
|
|
4720
|
+
sibling has an input that could produce this line. It mirrors the
|
|
4721
|
+
bare-choice refusal, which is the same mis-declaration one keyword over.
|
|
4722
|
+
"""
|
|
4723
|
+
raise ValueError(
|
|
4724
|
+
f'{surface} "{name}": retired_choices entry {index} is a bare value: '
|
|
4725
|
+
f"declare it as {_RETIRED_RECORD_SPELLING}"
|
|
4726
|
+
)
|
|
4727
|
+
|
|
4728
|
+
|
|
4729
|
+
def _resolve_retired_choices(
|
|
4730
|
+
surface: str, name: str, entries: object,
|
|
4731
|
+
) -> tuple["RetiredChoice", ...]:
|
|
4732
|
+
"""Validate a ``retired_choices=`` list's entry SHAPE.
|
|
4733
|
+
|
|
4734
|
+
The per-entry RULES (a live spelling, a duplicate, an empty message) are
|
|
4735
|
+
checked by ``_validate_retired_choices`` once the live choices are known.
|
|
4736
|
+
"""
|
|
4737
|
+
if not isinstance(entries, list):
|
|
4738
|
+
_raise_retired_choices_entry_not_record(surface, name, 0)
|
|
4739
|
+
records: list[RetiredChoice] = []
|
|
4740
|
+
for i, entry in enumerate(entries):
|
|
4741
|
+
if not isinstance(entry, RetiredChoice):
|
|
4742
|
+
_raise_retired_choices_entry_not_record(surface, name, i)
|
|
4743
|
+
records.append(entry)
|
|
4744
|
+
return tuple(records)
|
|
4745
|
+
|
|
4746
|
+
|
|
4747
|
+
# The registration-time templates, twinned per surface the way Go's err* and
|
|
4748
|
+
# TypeScript's err* functions are. Python could parameterize the Flag/Arg
|
|
4749
|
+
# prefix into one template, and does elsewhere; here it inlines both so the
|
|
4750
|
+
# three implementations share one signature per rule and the parity manifest
|
|
4751
|
+
# carries no entry for the family at all.
|
|
4752
|
+
|
|
4753
|
+
|
|
4754
|
+
def _raise_flag_retired_choice_is_live(name: str, value: str):
|
|
4755
|
+
raise ValueError(
|
|
4756
|
+
f'Flag "{name}": retired choice \'{value}\' is also a live choice: '
|
|
4757
|
+
f"a value is live or retired, never both"
|
|
4758
|
+
)
|
|
4759
|
+
|
|
4760
|
+
|
|
4761
|
+
def _raise_arg_retired_choice_is_live(name: str, value: str):
|
|
4762
|
+
raise ValueError(
|
|
4763
|
+
f'Arg "{name}": retired choice \'{value}\' is also a live choice: '
|
|
4764
|
+
f"a value is live or retired, never both"
|
|
4765
|
+
)
|
|
4766
|
+
|
|
4767
|
+
|
|
4768
|
+
def _raise_flag_retired_choice_duplicate(name: str, value: str):
|
|
4769
|
+
raise ValueError(f'Flag "{name}": retired choice \'{value}\' is declared twice')
|
|
4770
|
+
|
|
4771
|
+
|
|
4772
|
+
def _raise_arg_retired_choice_duplicate(name: str, value: str):
|
|
4773
|
+
raise ValueError(f'Arg "{name}": retired choice \'{value}\' is declared twice')
|
|
4774
|
+
|
|
4775
|
+
|
|
4776
|
+
def _raise_flag_retired_choice_message_empty(name: str, value: str):
|
|
4777
|
+
raise ValueError(
|
|
4778
|
+
f'Flag "{name}": retired choice \'{value}\': message must be a non-empty string'
|
|
4779
|
+
)
|
|
4780
|
+
|
|
4781
|
+
|
|
4782
|
+
def _raise_arg_retired_choice_message_empty(name: str, value: str):
|
|
4783
|
+
raise ValueError(
|
|
4784
|
+
f'Arg "{name}": retired choice \'{value}\': message must be a non-empty string'
|
|
4785
|
+
)
|
|
4786
|
+
|
|
4787
|
+
|
|
4788
|
+
def _raise_flag_retired_choices_incompatible_bool(name: str):
|
|
4789
|
+
raise ValueError(f'Flag "{name}": retired choices are incompatible with type=bool')
|
|
4790
|
+
|
|
4791
|
+
|
|
4792
|
+
def _raise_arg_retired_choices_incompatible_bool(name: str):
|
|
4793
|
+
raise ValueError(f'Arg "{name}": retired choices are incompatible with type=bool')
|
|
4794
|
+
|
|
4795
|
+
|
|
4796
|
+
def _raise_flag_default_is_retired_choice(name: str, value: str):
|
|
4797
|
+
raise ValueError(f'Flag "{name}": default \'{value}\' is a retired choice')
|
|
4798
|
+
|
|
4799
|
+
|
|
4800
|
+
def _raise_arg_default_is_retired_choice(name: str, value: str):
|
|
4801
|
+
raise ValueError(f'Arg "{name}": default \'{value}\' is a retired choice')
|
|
4802
|
+
|
|
4803
|
+
|
|
4804
|
+
def _raise_flag_retired_choice_type_mismatch(name: str, value: str, type_name: str):
|
|
4805
|
+
raise ValueError(
|
|
4806
|
+
f'Flag "{name}": retired choice \'{value}\' is not of type {type_name}'
|
|
4807
|
+
)
|
|
4808
|
+
|
|
4809
|
+
|
|
4810
|
+
def _raise_arg_retired_choice_type_mismatch(name: str, value: str, type_name: str):
|
|
4811
|
+
raise ValueError(
|
|
4812
|
+
f'Arg "{name}": retired choice \'{value}\' is not of type {type_name}'
|
|
4813
|
+
)
|
|
4814
|
+
|
|
4815
|
+
|
|
4816
|
+
def _raise_flag_retired_choices_require_choices(name: str):
|
|
4817
|
+
raise ValueError(f'Flag "{name}": retired choices require choices')
|
|
4818
|
+
|
|
4819
|
+
|
|
4820
|
+
def _raise_arg_retired_choices_require_choices(name: str):
|
|
4821
|
+
raise ValueError(f'Arg "{name}": retired choices require choices')
|
|
4822
|
+
|
|
4823
|
+
|
|
4824
|
+
_RETIRED_CHOICE_TEMPLATES = {
|
|
4825
|
+
"Flag": (
|
|
4826
|
+
_raise_flag_retired_choice_is_live,
|
|
4827
|
+
_raise_flag_retired_choice_duplicate,
|
|
4828
|
+
_raise_flag_retired_choice_message_empty,
|
|
4829
|
+
_raise_flag_retired_choices_incompatible_bool,
|
|
4830
|
+
_raise_flag_default_is_retired_choice,
|
|
4831
|
+
_raise_flag_retired_choices_require_choices,
|
|
4832
|
+
_raise_flag_retired_choice_type_mismatch,
|
|
4833
|
+
),
|
|
4834
|
+
"Arg": (
|
|
4835
|
+
_raise_arg_retired_choice_is_live,
|
|
4836
|
+
_raise_arg_retired_choice_duplicate,
|
|
4837
|
+
_raise_arg_retired_choice_message_empty,
|
|
4838
|
+
_raise_arg_retired_choices_incompatible_bool,
|
|
4839
|
+
_raise_arg_default_is_retired_choice,
|
|
4840
|
+
_raise_arg_retired_choices_require_choices,
|
|
4841
|
+
_raise_arg_retired_choice_type_mismatch,
|
|
4842
|
+
),
|
|
4843
|
+
}
|
|
4844
|
+
|
|
4845
|
+
|
|
4846
|
+
def _validate_retired_choices(
|
|
4847
|
+
surface: str,
|
|
4848
|
+
name: str,
|
|
4849
|
+
retired: tuple["RetiredChoice", ...] | None,
|
|
4850
|
+
choices: list | None,
|
|
4851
|
+
item_type: type,
|
|
4852
|
+
has_default: bool,
|
|
4853
|
+
default: object,
|
|
4854
|
+
) -> None:
|
|
4855
|
+
"""The registration-time guards, one set over both surfaces.
|
|
4856
|
+
|
|
4857
|
+
Every sentence names the CONCEPT ("retired choice") rather than this
|
|
4858
|
+
language's spelling of the declaration, so all three implementations share
|
|
4859
|
+
one signature per rule.
|
|
4860
|
+
"""
|
|
4861
|
+
if retired is None:
|
|
4862
|
+
return
|
|
4863
|
+
(
|
|
4864
|
+
is_live, duplicate, message_empty,
|
|
4865
|
+
incompatible_bool, default_is_retired, require_choices, type_mismatch,
|
|
4866
|
+
) = _RETIRED_CHOICE_TEMPLATES[surface]
|
|
4867
|
+
# The bool refusal comes first so the declaration is named by what it got
|
|
4868
|
+
# wrong: choices are already incompatible with bool, and reporting the
|
|
4869
|
+
# missing choices instead would send a reader to add a declaration the
|
|
4870
|
+
# framework would then refuse for the same reason.
|
|
4871
|
+
if item_type is bool:
|
|
4872
|
+
incompatible_bool(name)
|
|
4873
|
+
if choices is None:
|
|
4874
|
+
require_choices(name)
|
|
4875
|
+
seen: list = []
|
|
4876
|
+
for rc in retired:
|
|
4877
|
+
formatted = _format_value_for_error(rc.value)
|
|
4878
|
+
# The type check comes first: a value of the wrong type can never match
|
|
4879
|
+
# at parse time, so the declaration is dead however it compares against
|
|
4880
|
+
# the live set or against its siblings.
|
|
4881
|
+
if not isinstance(rc.value, item_type):
|
|
4882
|
+
type_mismatch(name, formatted, item_type.__name__)
|
|
4883
|
+
if not isinstance(rc.message, str) or not rc.message.strip():
|
|
4884
|
+
message_empty(name, formatted)
|
|
4885
|
+
if rc.value in choices:
|
|
4886
|
+
is_live(name, formatted)
|
|
4887
|
+
if rc.value in seen:
|
|
4888
|
+
duplicate(name, formatted)
|
|
4889
|
+
seen.append(rc.value)
|
|
4890
|
+
if has_default and default is not None:
|
|
4891
|
+
for rc in retired:
|
|
4892
|
+
if rc.value == default:
|
|
4893
|
+
default_is_retired(name, _format_value_for_error(default))
|
|
4894
|
+
|
|
4895
|
+
|
|
4896
|
+
def _retired_choice_message(
|
|
4897
|
+
value: object, retired: tuple["RetiredChoice", ...] | None,
|
|
4898
|
+
) -> tuple[str, bool]:
|
|
4899
|
+
"""The message declared for a retired spelling, and whether it is retired.
|
|
4900
|
+
|
|
4901
|
+
Retired lists are short and ordered, so the scan mirrors the choices one
|
|
4902
|
+
rather than building a map.
|
|
4903
|
+
"""
|
|
4904
|
+
if retired is None:
|
|
4905
|
+
return "", False
|
|
4906
|
+
for rc in retired:
|
|
4907
|
+
if value == rc.value and type(value) is type(rc.value):
|
|
4908
|
+
return rc.message, True
|
|
4909
|
+
return "", False
|
|
4910
|
+
|
|
4911
|
+
|
|
4639
4912
|
def _raise_choices_entry_not_record(surface: str, name: str, index: int):
|
|
4640
4913
|
"""Message template: a bare `choices=` entry (contract §12.13)."""
|
|
4641
4914
|
raise ValueError(
|
|
@@ -4761,6 +5034,10 @@ class Flag:
|
|
|
4761
5034
|
# value list so per-entry help survives to help rendering. Set by
|
|
4762
5035
|
# __post_init__ from `choices`, never by the caller.
|
|
4763
5036
|
choice_records: tuple["Choice", ...] | None = None
|
|
5037
|
+
# The spellings this flag USED to accept, each carrying the message that
|
|
5038
|
+
# names its replacement. A retired value is refused at parse time; it is
|
|
5039
|
+
# not a choice, so help and the published value_schema never name it.
|
|
5040
|
+
retired_choices: list | None = None
|
|
4764
5041
|
|
|
4765
5042
|
def __post_init__(self) -> None:
|
|
4766
5043
|
_require_non_empty_str(self.help, "help", "Flag")
|
|
@@ -4943,6 +5220,20 @@ class Flag:
|
|
|
4943
5220
|
f'Flag "{self.name}": type=float requires a float default, '
|
|
4944
5221
|
f"got {type(self.default).__name__!r}"
|
|
4945
5222
|
)
|
|
5223
|
+
# Retired choices. The entry SHAPE is resolved first, then the rules --
|
|
5224
|
+
# after the live choices are validated, so a declaration that got its
|
|
5225
|
+
# choices wrong is told that first, and BEFORE the default-in-choices
|
|
5226
|
+
# check, so a default naming a retired spelling is answered by the
|
|
5227
|
+
# sentence that names the reason.
|
|
5228
|
+
if self.retired_choices is not None:
|
|
5229
|
+
self.retired_choices = _resolve_retired_choices(
|
|
5230
|
+
"Flag", self.name, self.retired_choices,
|
|
5231
|
+
)
|
|
5232
|
+
_validate_retired_choices(
|
|
5233
|
+
"Flag", self.name, self.retired_choices, self.choices,
|
|
5234
|
+
self.item_type if self.compound == "list" else self.type,
|
|
5235
|
+
self.presence == _PRESENCE_DEFAULT, self.default,
|
|
5236
|
+
)
|
|
4946
5237
|
# Validate default is in choices. The check applies to declared VALUES
|
|
4947
5238
|
# only: a required or optional flag has no value to check, and absence
|
|
4948
5239
|
# is never matched against choices (§23.5).
|
|
@@ -4979,6 +5270,8 @@ class Arg:
|
|
|
4979
5270
|
item_type: type | None = None
|
|
4980
5271
|
# The declared `choices=` records (contract §24.2), same as a flag's.
|
|
4981
5272
|
choice_records: tuple["Choice", ...] | None = None
|
|
5273
|
+
# The arg twin of Flag.retired_choices.
|
|
5274
|
+
retired_choices: list | None = None
|
|
4982
5275
|
|
|
4983
5276
|
def __post_init__(self) -> None:
|
|
4984
5277
|
_require_non_empty_str(self.help, "help", "Arg")
|
|
@@ -5075,6 +5368,17 @@ class Arg:
|
|
|
5075
5368
|
f'Arg "{self.name}": type=str requires a str default, '
|
|
5076
5369
|
f"got {type(self.default).__name__!r}"
|
|
5077
5370
|
)
|
|
5371
|
+
# Retired choices, ahead of the default-in-choices check for the reason
|
|
5372
|
+
# stated at the flag surface.
|
|
5373
|
+
if self.retired_choices is not None:
|
|
5374
|
+
self.retired_choices = _resolve_retired_choices(
|
|
5375
|
+
"Arg", self.name, self.retired_choices,
|
|
5376
|
+
)
|
|
5377
|
+
_validate_retired_choices(
|
|
5378
|
+
"Arg", self.name, self.retired_choices, self.choices,
|
|
5379
|
+
self.item_type if self.compound == "list" else self.type,
|
|
5380
|
+
self.presence == _PRESENCE_DEFAULT, self.default,
|
|
5381
|
+
)
|
|
5078
5382
|
# Validate default is in choices -- declared VALUES only (§23.5)
|
|
5079
5383
|
if self.choices is not None and self.presence == _PRESENCE_DEFAULT:
|
|
5080
5384
|
if self.default not in self.choices:
|
|
@@ -9228,7 +9532,9 @@ class App:
|
|
|
9228
9532
|
ctx.effects", so a leaf the handle could not carry must never be a
|
|
9229
9533
|
finding: the handle's closed method set has no in-process-observe
|
|
9230
9534
|
method, which is why ``platform.system()`` is exempt while
|
|
9231
|
-
``os.system(...)`` is not (see :data:`_BYPASS_PROCESS_OS_ONLY`)
|
|
9535
|
+
``os.system(...)`` is not (see :data:`_BYPASS_PROCESS_OS_ONLY`). It
|
|
9536
|
+
reads only repository-owned files (see :func:`_bypass_repo_files`)
|
|
9537
|
+
and refuses a project root outside a git work tree;
|
|
9232
9538
|
- ``observe-allowlist-breadth`` (warn) surfaces short
|
|
9233
9539
|
``proc_observe_allowlist`` prefixes, which authorize real execution
|
|
9234
9540
|
under ``--dry-run``;
|
|
@@ -9237,7 +9543,13 @@ class App:
|
|
|
9237
9543
|
themselves consequential.
|
|
9238
9544
|
"""
|
|
9239
9545
|
def impl(ctx: CheckContext, reporter: "ErrorReporter") -> "_CheckOutcome":
|
|
9240
|
-
|
|
9546
|
+
try:
|
|
9547
|
+
findings = _scan_effects_bypasses(Path(ctx.project_root))
|
|
9548
|
+
except _BypassRootNotAWorkTree as exc:
|
|
9549
|
+
# The input rule is part of the verdict: a root the repository
|
|
9550
|
+
# does not own is refused here, never scanned another way.
|
|
9551
|
+
reporter.error(str(exc))
|
|
9552
|
+
return reporter.found(str(exc))
|
|
9241
9553
|
for rel, lineno, func_name, target in findings:
|
|
9242
9554
|
reporter.error(
|
|
9243
9555
|
f"{rel}:{lineno}: {func_name} calls {target} directly; "
|
|
@@ -11419,7 +11731,10 @@ class App:
|
|
|
11419
11731
|
# Validate choices for global flags
|
|
11420
11732
|
for f in self._global_flags:
|
|
11421
11733
|
if f.name in cli_set:
|
|
11422
|
-
_validate_choices(
|
|
11734
|
+
_validate_choices(
|
|
11735
|
+
f.name, cli_set[f.name], f.repeatable, f.choices,
|
|
11736
|
+
f.retired_choices,
|
|
11737
|
+
)
|
|
11423
11738
|
|
|
11424
11739
|
return cli_set, global_sources, remaining
|
|
11425
11740
|
|
|
@@ -12734,6 +13049,7 @@ def _validate_choices(
|
|
|
12734
13049
|
val: object,
|
|
12735
13050
|
repeatable: bool,
|
|
12736
13051
|
choices: list | None,
|
|
13052
|
+
retired: tuple["RetiredChoice", ...] | None = None,
|
|
12737
13053
|
*,
|
|
12738
13054
|
is_arg: bool = False,
|
|
12739
13055
|
) -> None:
|
|
@@ -12745,12 +13061,26 @@ def _validate_choices(
|
|
|
12745
13061
|
A None value is exempt from validation: None only arises when the flag or
|
|
12746
13062
|
arg was not passed (an unset mutex flag, or default=None on an arg) -- a
|
|
12747
13063
|
CLI-supplied value is never None.
|
|
13064
|
+
|
|
13065
|
+
A RETIRED spelling is checked first, so a reader who typed a value that
|
|
13066
|
+
used to work is told what replaced it instead of being handed the list it
|
|
13067
|
+
is missing from. Every source that reaches this funnel today -- command
|
|
13068
|
+
line, env var, config file, and the programmatic doors -- takes the retired
|
|
13069
|
+
refusal for free; nothing new is resolved here.
|
|
12748
13070
|
"""
|
|
12749
|
-
if choices is None or val is None:
|
|
13071
|
+
if (choices is None and retired is None) or val is None:
|
|
12750
13072
|
return
|
|
12751
13073
|
vals = val if repeatable else [val]
|
|
12752
13074
|
for v in vals:
|
|
12753
|
-
|
|
13075
|
+
message, is_retired = _retired_choice_message(v, retired)
|
|
13076
|
+
if is_retired:
|
|
13077
|
+
v_str = _format_value_for_error(v)
|
|
13078
|
+
if is_arg:
|
|
13079
|
+
raise _ParseError(
|
|
13080
|
+
f"argument '{name}': value '{v_str}' retired: {message}"
|
|
13081
|
+
)
|
|
13082
|
+
raise _ParseError(f"--{name}: value '{v_str}' retired: {message}")
|
|
13083
|
+
if choices is not None and v not in choices:
|
|
12754
13084
|
choices_str = ", ".join(
|
|
12755
13085
|
_format_float_canonical(c) if isinstance(c, float) else str(c)
|
|
12756
13086
|
for c in choices
|
|
@@ -12913,7 +13243,10 @@ def _validate_and_build_kwargs(
|
|
|
12913
13243
|
# Step 5.5: validate choices
|
|
12914
13244
|
for f in cmd.flags:
|
|
12915
13245
|
if store.has(f.name):
|
|
12916
|
-
_validate_choices(
|
|
13246
|
+
_validate_choices(
|
|
13247
|
+
f.name, store[f.name], f.repeatable, f.choices,
|
|
13248
|
+
f.retired_choices,
|
|
13249
|
+
)
|
|
12917
13250
|
|
|
12918
13251
|
# Step 5.6: custom validation. It runs on a SUPPLIED value only: never on
|
|
12919
13252
|
# absence, and never on a declared default (§23.5's validate row).
|
|
@@ -12996,7 +13329,8 @@ def _validate_and_build_kwargs(
|
|
|
12996
13329
|
for a in cmd.args:
|
|
12997
13330
|
if a.name in arg_values:
|
|
12998
13331
|
_validate_choices(
|
|
12999
|
-
a.name, arg_values[a.name], a.variadic, a.choices,
|
|
13332
|
+
a.name, arg_values[a.name], a.variadic, a.choices,
|
|
13333
|
+
a.retired_choices, is_arg=True,
|
|
13000
13334
|
)
|
|
13001
13335
|
|
|
13002
13336
|
# Step 7: build kwargs dict (command flags only)
|
|
@@ -13773,7 +14107,7 @@ def _check_scoped_value(
|
|
|
13773
14107
|
set and its callback both apply, exactly as they do on the root surface
|
|
13774
14108
|
(steps 5.5 and 5.6). Only a declared default escapes ``validate``.
|
|
13775
14109
|
"""
|
|
13776
|
-
_validate_choices(f.name, value, f.repeatable, f.choices)
|
|
14110
|
+
_validate_choices(f.name, value, f.repeatable, f.choices, f.retired_choices)
|
|
13777
14111
|
if f.validate is not None and value is not None:
|
|
13778
14112
|
for v in (value if f.repeatable else [value]):
|
|
13779
14113
|
try:
|
|
@@ -15606,6 +15940,7 @@ def flag(
|
|
|
15606
15940
|
prefixed: bool = True,
|
|
15607
15941
|
negatable: object = _MISSING,
|
|
15608
15942
|
choices: list | None = None,
|
|
15943
|
+
retired_choices: list | None = None,
|
|
15609
15944
|
validate: Callable | None = None,
|
|
15610
15945
|
repeatable: bool = False,
|
|
15611
15946
|
unique: object = _MISSING,
|
|
@@ -15629,6 +15964,7 @@ def flag(
|
|
|
15629
15964
|
prefixed=prefixed,
|
|
15630
15965
|
negatable=negatable,
|
|
15631
15966
|
choices=choices,
|
|
15967
|
+
retired_choices=retired_choices,
|
|
15632
15968
|
validate=validate,
|
|
15633
15969
|
repeatable=repeatable,
|
|
15634
15970
|
unique=unique,
|
|
@@ -15654,6 +15990,7 @@ def arg(
|
|
|
15654
15990
|
variadic: bool = False,
|
|
15655
15991
|
type: type = str,
|
|
15656
15992
|
choices: list | None = None,
|
|
15993
|
+
retired_choices: list | None = None,
|
|
15657
15994
|
) -> Callable[[F], F]:
|
|
15658
15995
|
"""Module-level decorator to attach an Arg to a command handler."""
|
|
15659
15996
|
|
|
@@ -15661,6 +15998,7 @@ def arg(
|
|
|
15661
15998
|
a = Arg(
|
|
15662
15999
|
name=name, help=help, presence=presence, default=default,
|
|
15663
16000
|
variadic=variadic, type=type, choices=choices,
|
|
16001
|
+
retired_choices=retired_choices,
|
|
15664
16002
|
)
|
|
15665
16003
|
if not hasattr(func, "_strictcli_args"):
|
|
15666
16004
|
func._strictcli_args = []
|
|
@@ -16859,6 +17197,8 @@ def _serialize_flag(f: Flag) -> dict:
|
|
|
16859
17197
|
d["prefixed"] = False
|
|
16860
17198
|
if f.choice_records is not None:
|
|
16861
17199
|
d["choices"] = _serialize_choice_records(f.choice_records)
|
|
17200
|
+
if f.retired_choices:
|
|
17201
|
+
d["retired_choices"] = _serialize_retired_choices(f.retired_choices)
|
|
16862
17202
|
if f.unique is True:
|
|
16863
17203
|
d["unique"] = True
|
|
16864
17204
|
# Per-flag conflict mode: serialized only when explicitly set. Absence
|
|
@@ -16880,6 +17220,20 @@ def _serialize_flag(f: Flag) -> dict:
|
|
|
16880
17220
|
return d
|
|
16881
17221
|
|
|
16882
17222
|
|
|
17223
|
+
def _serialize_retired_choices(retired: tuple["RetiredChoice", ...]) -> dict:
|
|
17224
|
+
"""A declaration's retired spellings, as a map from spelling to message.
|
|
17225
|
+
|
|
17226
|
+
SORTED ascending by key -- the treatment a group's `deprecated` map already
|
|
17227
|
+
gets, and for the same reason: a keyed object whose declaration order no
|
|
17228
|
+
implementation is required to retain has sort order as its only reachable
|
|
17229
|
+
canon. The key is the spelling rendered through the error-value formatter,
|
|
17230
|
+
so an int, a float and a string key identically in all three
|
|
17231
|
+
implementations. The map is omitted entirely when nothing is retired.
|
|
17232
|
+
"""
|
|
17233
|
+
messages = {_format_value_for_error(rc.value): rc.message for rc in retired}
|
|
17234
|
+
return {key: messages[key] for key in sorted(messages)}
|
|
17235
|
+
|
|
17236
|
+
|
|
16883
17237
|
def _serialize_choice_object(c: "_ChoiceSpec") -> dict:
|
|
16884
17238
|
"""One choice of one selector: `name`, `help`, and its scope (§25.6).
|
|
16885
17239
|
|
|
@@ -16982,6 +17336,8 @@ def _serialize_arg(a: Arg) -> dict:
|
|
|
16982
17336
|
d["variadic"] = a.variadic
|
|
16983
17337
|
if a.choice_records is not None:
|
|
16984
17338
|
d["choices"] = _serialize_choice_records(a.choice_records)
|
|
17339
|
+
if a.retired_choices:
|
|
17340
|
+
d["retired_choices"] = _serialize_retired_choices(a.retired_choices)
|
|
16985
17341
|
return d
|
|
16986
17342
|
|
|
16987
17343
|
|