strictcli 0.8.7__tar.gz → 0.9.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.9.0/.rlsbl/changes/.validated +1 -0
- strictcli-0.9.0/.rlsbl/changes/0.9.0.jsonl +5 -0
- strictcli-0.9.0/.rlsbl/changes/0.9.0.md +5 -0
- strictcli-0.9.0/.rlsbl/releases/v0.9.0.toml +3 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/CHANGELOG.md +6 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/PKG-INFO +1 -1
- {strictcli-0.8.7 → strictcli-0.9.0}/package-lock.json +2 -2
- {strictcli-0.8.7 → strictcli-0.9.0}/package.json +1 -1
- {strictcli-0.8.7 → strictcli-0.9.0}/pyproject.toml +1 -1
- {strictcli-0.8.7 → strictcli-0.9.0}/strictcli/__init__.py +106 -23
- {strictcli-0.8.7 → strictcli-0.9.0}/tests/test_config.py +325 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/uv.lock +1 -1
- strictcli-0.8.7/.rlsbl/changes/.validated +0 -1
- {strictcli-0.8.7 → strictcli-0.9.0}/.claude/settings.json +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/.github/workflows/ci.yml +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/.github/workflows/publish.yml +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/.gitignore +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/bases/.claude/settings.json +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/bases/.github/workflows/ci.yml +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/bases/.github/workflows/publish.yml +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/bases/.gitignore +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/bases/.rlsbl/hooks/post-release.sh +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/bases/.rlsbl/hooks/pre-checks.sh +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/bases/.rlsbl/hooks/pre-release.sh +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/bases/.rlsbl/lint/go.toml +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/bases/.rlsbl/lint/npm.toml +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/bases/.rlsbl/lint/python.toml +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/bases/CHANGELOG.md +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/bases/CLAUDE.md +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/bases/LICENSE +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/changes/0.4.0.jsonl +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/changes/0.4.0.md +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/changes/0.4.1.jsonl +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/changes/0.4.1.md +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/changes/0.5.0.jsonl +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/changes/0.5.0.md +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/changes/0.6.0.jsonl +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/changes/0.6.0.md +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/changes/0.6.1.jsonl +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/changes/0.6.1.md +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/changes/0.7.0.jsonl +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/changes/0.7.0.md +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/changes/0.7.1.jsonl +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/changes/0.7.1.md +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/changes/0.8.0.jsonl +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/changes/0.8.0.md +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/changes/0.8.1.jsonl +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/changes/0.8.1.md +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/changes/0.8.2.jsonl +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/changes/0.8.2.md +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/changes/0.8.3.jsonl +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/changes/0.8.3.md +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/changes/0.8.4.jsonl +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/changes/0.8.4.md +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/changes/0.8.5.jsonl +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/changes/0.8.5.md +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/changes/0.8.6.jsonl +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/changes/0.8.6.md +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/changes/0.8.7.jsonl +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/changes/0.8.7.md +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/changes/unreleased.jsonl +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/config.json +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/hashes.json +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/hooks/post-release.sh +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/hooks/pre-checks.sh +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/hooks/pre-release.sh +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/lint/go.toml +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/lint/npm.toml +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/lint/python.toml +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/releases/unreleased.toml +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/releases/v0.8.5.toml +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/releases/v0.8.6.toml +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/releases/v0.8.7.toml +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/version +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/.strictcli/schema.json +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/CLAUDE.md +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/LICENSE +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/README.md +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/index.js +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/postinstall.js +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/tests/test_arg_default.py +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/tests/test_auto_version.py +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/tests/test_check_command.py +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/tests/test_check_discovery.py +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/tests/test_check_runner.py +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/tests/test_check_schema.py +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/tests/test_check_types.py +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/tests/test_choices.py +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/tests/test_command_help_suggestion.py +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/tests/test_deep_nesting.py +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/tests/test_dependencies.py +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/tests/test_deprecated.py +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/tests/test_dump_schema.py +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/tests/test_e2e.py +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/tests/test_env.py +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/tests/test_exit_codes.py +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/tests/test_float_type.py +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/tests/test_global_flags.py +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/tests/test_help.py +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/tests/test_int_type.py +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/tests/test_mutex.py +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/tests/test_nesting.py +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/tests/test_parser.py +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/tests/test_passthrough.py +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/tests/test_registration.py +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/tests/test_repeatable.py +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/tests/test_tagdsl.py +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/tests/test_tags.py +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/tests/test_toml_loading.py +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/tests/test_validate.py +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/tests/test_variadic.py +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/todo/.defer/deferred.md +0 -0
- {strictcli-0.8.7 → strictcli-0.9.0}/todo/.done/original-idea.md +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
aeaf4155c6c810b0aff48faa3574b04897413861
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
{"commits":["e595eaf58390d6b941b0362dfcbc65b9016054a9"],"user_facing":false}
|
|
2
|
+
{"commits":["36342216953c20b8f38c9517e4eebad0893d1718"],"user_facing":false}
|
|
3
|
+
{"commits":["fad001e35420a4aa0bd019a68448ce4c970d3f1e"],"user_facing":false}
|
|
4
|
+
{"commits":["bb95ff55342e50eeec9a20c417b96e76df99e9c0"],"user_facing":true,"description":"**New feature.** config_path and config_format options for TOML-based configuration file support.","type":"feature"}
|
|
5
|
+
{"commits":["0f0524d47831254eb0579b55ea14aff204043546"],"user_facing":false}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "strictcli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "strictcli",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.9.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.9.0"
|
|
6
6
|
|
|
7
7
|
__all__ = [
|
|
8
8
|
"App", "Flag", "Arg", "Tag", "MutexGroup", "CoRequired", "Requires",
|
|
@@ -36,21 +36,40 @@ class _MissingSentinel:
|
|
|
36
36
|
_MISSING = _MissingSentinel()
|
|
37
37
|
|
|
38
38
|
|
|
39
|
-
def _config_path(app_name: str) -> str:
|
|
40
|
-
"""Compute the config file path for an app.
|
|
39
|
+
def _config_path(app_name: str, *, override: str | None = None, config_format: str = "json") -> str:
|
|
40
|
+
"""Compute the config file path for an app.
|
|
41
|
+
|
|
42
|
+
If override is provided, expand ~ and return it directly.
|
|
43
|
+
Otherwise compute from XDG_CONFIG_HOME + app_name.
|
|
44
|
+
"""
|
|
45
|
+
if override is not None:
|
|
46
|
+
return os.path.expanduser(override)
|
|
41
47
|
config_home = os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config"))
|
|
42
|
-
|
|
48
|
+
ext = "toml" if config_format == "toml" else "json"
|
|
49
|
+
return os.path.join(config_home, app_name, f"config.{ext}")
|
|
43
50
|
|
|
44
51
|
|
|
45
|
-
def _load_config(
|
|
46
|
-
|
|
52
|
+
def _load_config(
|
|
53
|
+
app_name: str,
|
|
54
|
+
*,
|
|
55
|
+
config_path_override: str | None = None,
|
|
56
|
+
config_format: str = "json",
|
|
57
|
+
) -> dict:
|
|
58
|
+
"""Load the config file for an app.
|
|
47
59
|
|
|
48
|
-
Returns an empty dict if the file doesn't exist or contains invalid
|
|
49
|
-
Invalid
|
|
60
|
+
Returns an empty dict if the file doesn't exist or contains invalid content.
|
|
61
|
+
Invalid content prints a warning to stderr.
|
|
50
62
|
"""
|
|
51
|
-
path = _config_path(app_name)
|
|
63
|
+
path = _config_path(app_name, override=config_path_override, config_format=config_format)
|
|
52
64
|
if not os.path.isfile(path):
|
|
53
65
|
return {}
|
|
66
|
+
if config_format == "toml":
|
|
67
|
+
try:
|
|
68
|
+
with open(path, "rb") as f:
|
|
69
|
+
return tomllib.load(f)
|
|
70
|
+
except (tomllib.TOMLDecodeError, UnicodeDecodeError):
|
|
71
|
+
print(f"warning: invalid TOML in config file '{path}', ignoring", file=sys.stderr)
|
|
72
|
+
return {}
|
|
54
73
|
try:
|
|
55
74
|
with open(path) as f:
|
|
56
75
|
return json.loads(f.read())
|
|
@@ -59,6 +78,29 @@ def _load_config(app_name: str) -> dict:
|
|
|
59
78
|
return {}
|
|
60
79
|
|
|
61
80
|
|
|
81
|
+
def _write_toml_flat(data: dict, path: str) -> None:
|
|
82
|
+
"""Write a flat dict as a TOML file.
|
|
83
|
+
|
|
84
|
+
Supports str, int, float, and bool values. This avoids requiring
|
|
85
|
+
a TOML writer dependency for the simple key=value configs that
|
|
86
|
+
'config set' produces.
|
|
87
|
+
"""
|
|
88
|
+
lines: list[str] = []
|
|
89
|
+
for key, value in data.items():
|
|
90
|
+
if isinstance(value, bool):
|
|
91
|
+
lines.append(f"{key} = {str(value).lower()}")
|
|
92
|
+
elif isinstance(value, str):
|
|
93
|
+
escaped = value.replace("\\", "\\\\").replace('"', '\\"')
|
|
94
|
+
lines.append(f'{key} = "{escaped}"')
|
|
95
|
+
elif isinstance(value, (int, float)):
|
|
96
|
+
lines.append(f"{key} = {value}")
|
|
97
|
+
else:
|
|
98
|
+
escaped = str(value).replace("\\", "\\\\").replace('"', '\\"')
|
|
99
|
+
lines.append(f'{key} = "{escaped}"')
|
|
100
|
+
with open(path, "w") as f:
|
|
101
|
+
f.write("\n".join(lines) + "\n" if lines else "")
|
|
102
|
+
|
|
103
|
+
|
|
62
104
|
def _coerce_config_value(value: object, flag: "Flag") -> object:
|
|
63
105
|
"""Coerce a JSON config value to the flag's type.
|
|
64
106
|
|
|
@@ -579,6 +621,8 @@ class App:
|
|
|
579
621
|
version: str | None = None
|
|
580
622
|
env_prefix: str | None = None
|
|
581
623
|
config: bool = False
|
|
624
|
+
config_path: str | None = None
|
|
625
|
+
config_format: str = "json"
|
|
582
626
|
flags: list[Flag] = field(default_factory=list)
|
|
583
627
|
_commands: dict[str, Command] = field(default_factory=dict)
|
|
584
628
|
_groups: dict[str, Group] = field(default_factory=dict)
|
|
@@ -600,10 +644,19 @@ class App:
|
|
|
600
644
|
seen.add(f.name)
|
|
601
645
|
self._global_flags: list[Flag] = list(self.flags)
|
|
602
646
|
self._last_global_values: dict[str, object] = {}
|
|
647
|
+
# Validate config_format
|
|
648
|
+
if self.config_format not in ("json", "toml"):
|
|
649
|
+
raise ValueError(
|
|
650
|
+
f'App.config_format must be "json" or "toml", got {self.config_format!r}'
|
|
651
|
+
)
|
|
603
652
|
# Load config and register config subcommands if enabled
|
|
604
653
|
self._config_data: dict = {}
|
|
605
654
|
if self.config:
|
|
606
|
-
self._config_data = _load_config(
|
|
655
|
+
self._config_data = _load_config(
|
|
656
|
+
self.name,
|
|
657
|
+
config_path_override=self.config_path,
|
|
658
|
+
config_format=self.config_format,
|
|
659
|
+
)
|
|
607
660
|
self._register_config_group()
|
|
608
661
|
# Discover checks TOML
|
|
609
662
|
self._check_context_factory: Callable | None = None
|
|
@@ -843,12 +896,20 @@ class App:
|
|
|
843
896
|
config_grp.commands["path"] = Command(
|
|
844
897
|
name="path",
|
|
845
898
|
help="Print the config file path",
|
|
846
|
-
handler=lambda **_kw: print(_config_path(
|
|
899
|
+
handler=lambda **_kw: print(_config_path(
|
|
900
|
+
app_ref.name,
|
|
901
|
+
override=app_ref.config_path,
|
|
902
|
+
config_format=app_ref.config_format,
|
|
903
|
+
)),
|
|
847
904
|
)
|
|
848
905
|
|
|
849
906
|
# config show
|
|
850
907
|
def _config_show_handler(**_kw) -> None:
|
|
851
|
-
config_data = _load_config(
|
|
908
|
+
config_data = _load_config(
|
|
909
|
+
app_ref.name,
|
|
910
|
+
config_path_override=app_ref.config_path,
|
|
911
|
+
config_format=app_ref.config_format,
|
|
912
|
+
)
|
|
852
913
|
all_flags = app_ref._collect_all_flags()
|
|
853
914
|
for f in all_flags:
|
|
854
915
|
param = _flag_param_name(f.name)
|
|
@@ -872,20 +933,34 @@ class App:
|
|
|
872
933
|
|
|
873
934
|
# config set
|
|
874
935
|
def _config_set_handler(key, value, **_kw) -> None:
|
|
875
|
-
path = _config_path(
|
|
936
|
+
path = _config_path(
|
|
937
|
+
app_ref.name,
|
|
938
|
+
override=app_ref.config_path,
|
|
939
|
+
config_format=app_ref.config_format,
|
|
940
|
+
)
|
|
876
941
|
dir_path = os.path.dirname(path)
|
|
877
942
|
os.makedirs(dir_path, exist_ok=True)
|
|
878
943
|
# Read existing config
|
|
879
944
|
existing: dict = {}
|
|
880
945
|
if os.path.isfile(path):
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
946
|
+
if app_ref.config_format == "toml":
|
|
947
|
+
try:
|
|
948
|
+
with open(path, "rb") as fh:
|
|
949
|
+
existing = tomllib.load(fh)
|
|
950
|
+
except (tomllib.TOMLDecodeError, UnicodeDecodeError):
|
|
951
|
+
existing = {}
|
|
952
|
+
else:
|
|
953
|
+
try:
|
|
954
|
+
with open(path) as fh:
|
|
955
|
+
existing = json.loads(fh.read())
|
|
956
|
+
except (json.JSONDecodeError, ValueError):
|
|
957
|
+
existing = {}
|
|
886
958
|
existing[key] = value
|
|
887
|
-
|
|
888
|
-
|
|
959
|
+
if app_ref.config_format == "toml":
|
|
960
|
+
_write_toml_flat(existing, path)
|
|
961
|
+
else:
|
|
962
|
+
with open(path, "w") as fh:
|
|
963
|
+
fh.write(json.dumps(existing, indent=2) + "\n")
|
|
889
964
|
|
|
890
965
|
config_grp.commands["set"] = Command(
|
|
891
966
|
name="set",
|
|
@@ -899,12 +974,20 @@ class App:
|
|
|
899
974
|
|
|
900
975
|
# config edit
|
|
901
976
|
def _config_edit_handler(**_kw) -> None:
|
|
902
|
-
path = _config_path(
|
|
977
|
+
path = _config_path(
|
|
978
|
+
app_ref.name,
|
|
979
|
+
override=app_ref.config_path,
|
|
980
|
+
config_format=app_ref.config_format,
|
|
981
|
+
)
|
|
903
982
|
dir_path = os.path.dirname(path)
|
|
904
983
|
os.makedirs(dir_path, exist_ok=True)
|
|
905
984
|
if not os.path.isfile(path):
|
|
906
|
-
|
|
907
|
-
|
|
985
|
+
if app_ref.config_format == "toml":
|
|
986
|
+
with open(path, "w") as fh:
|
|
987
|
+
fh.write("")
|
|
988
|
+
else:
|
|
989
|
+
with open(path, "w") as fh:
|
|
990
|
+
fh.write("{}\n")
|
|
908
991
|
editor = os.environ.get("EDITOR", "vi")
|
|
909
992
|
subprocess.run([editor, path])
|
|
910
993
|
|
|
@@ -376,3 +376,328 @@ def test_config_int_as_float(tmp_path, monkeypatch):
|
|
|
376
376
|
r = app.test(["run"])
|
|
377
377
|
assert r.exit_code == 0
|
|
378
378
|
assert "ratio=2.0" in r.stdout
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
# --- Custom config_path tests ---
|
|
382
|
+
|
|
383
|
+
def test_custom_config_path(tmp_path, monkeypatch):
|
|
384
|
+
"""Custom config_path is used instead of XDG-computed path."""
|
|
385
|
+
config_file = tmp_path / "my-custom-config.json"
|
|
386
|
+
config_file.write_text(json.dumps({"target": "custom-path-val"}) + "\n")
|
|
387
|
+
|
|
388
|
+
app = strictcli.App(
|
|
389
|
+
name="testapp",
|
|
390
|
+
version="1.0.0",
|
|
391
|
+
help="test app",
|
|
392
|
+
config=True,
|
|
393
|
+
config_path=str(config_file),
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
@app.command("run", help="run something")
|
|
397
|
+
@strictcli.flag("target", type=str, help="the target", default="default-val")
|
|
398
|
+
def run(target):
|
|
399
|
+
print(f"target={target}")
|
|
400
|
+
|
|
401
|
+
r = app.test(["run"])
|
|
402
|
+
assert r.exit_code == 0
|
|
403
|
+
assert "target=custom-path-val" in r.stdout
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def test_custom_config_path_tilde_expansion(tmp_path, monkeypatch):
|
|
407
|
+
"""Custom config_path expands ~ correctly."""
|
|
408
|
+
monkeypatch.setenv("HOME", str(tmp_path))
|
|
409
|
+
config_dir = tmp_path / ".myapp"
|
|
410
|
+
config_dir.mkdir()
|
|
411
|
+
config_file = config_dir / "settings.json"
|
|
412
|
+
config_file.write_text(json.dumps({"target": "tilde-val"}) + "\n")
|
|
413
|
+
|
|
414
|
+
app = strictcli.App(
|
|
415
|
+
name="testapp",
|
|
416
|
+
version="1.0.0",
|
|
417
|
+
help="test app",
|
|
418
|
+
config=True,
|
|
419
|
+
config_path="~/.myapp/settings.json",
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
@app.command("run", help="run something")
|
|
423
|
+
@strictcli.flag("target", type=str, help="the target", default="default-val")
|
|
424
|
+
def run(target):
|
|
425
|
+
print(f"target={target}")
|
|
426
|
+
|
|
427
|
+
r = app.test(["run"])
|
|
428
|
+
assert r.exit_code == 0
|
|
429
|
+
assert "target=tilde-val" in r.stdout
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def test_custom_config_path_config_path_command(tmp_path):
|
|
433
|
+
"""config path command prints the custom path."""
|
|
434
|
+
config_file = tmp_path / "custom.json"
|
|
435
|
+
config_file.write_text("{}")
|
|
436
|
+
|
|
437
|
+
app = strictcli.App(
|
|
438
|
+
name="testapp",
|
|
439
|
+
version="1.0.0",
|
|
440
|
+
help="test app",
|
|
441
|
+
config=True,
|
|
442
|
+
config_path=str(config_file),
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
@app.command("run", help="run something")
|
|
446
|
+
def run():
|
|
447
|
+
pass
|
|
448
|
+
|
|
449
|
+
r = app.test(["config", "path"])
|
|
450
|
+
assert r.exit_code == 0
|
|
451
|
+
assert str(config_file) in r.stdout
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def test_custom_config_path_config_set(tmp_path):
|
|
455
|
+
"""config set writes to the custom path."""
|
|
456
|
+
config_file = tmp_path / "custom.json"
|
|
457
|
+
|
|
458
|
+
app = strictcli.App(
|
|
459
|
+
name="testapp",
|
|
460
|
+
version="1.0.0",
|
|
461
|
+
help="test app",
|
|
462
|
+
config=True,
|
|
463
|
+
config_path=str(config_file),
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
@app.command("run", help="run something")
|
|
467
|
+
@strictcli.flag("target", type=str, help="the target", default="default-val")
|
|
468
|
+
def run(target):
|
|
469
|
+
print(f"target={target}")
|
|
470
|
+
|
|
471
|
+
r = app.test(["config", "set", "target", "written"])
|
|
472
|
+
assert r.exit_code == 0
|
|
473
|
+
assert config_file.exists()
|
|
474
|
+
data = json.loads(config_file.read_text())
|
|
475
|
+
assert data["target"] == "written"
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
# --- TOML config format tests ---
|
|
479
|
+
|
|
480
|
+
def test_toml_format_reads_correctly(tmp_path):
|
|
481
|
+
"""TOML format config reads values correctly."""
|
|
482
|
+
config_file = tmp_path / "config.toml"
|
|
483
|
+
config_file.write_text('target = "toml-value"\ncount = 42\nverbose = true\n')
|
|
484
|
+
|
|
485
|
+
app = strictcli.App(
|
|
486
|
+
name="testapp",
|
|
487
|
+
version="1.0.0",
|
|
488
|
+
help="test app",
|
|
489
|
+
config=True,
|
|
490
|
+
config_path=str(config_file),
|
|
491
|
+
config_format="toml",
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
@app.command("run", help="run something")
|
|
495
|
+
@strictcli.flag("target", type=str, help="the target", default="default-val")
|
|
496
|
+
@strictcli.flag("count", type=int, help="how many", default=1)
|
|
497
|
+
@strictcli.flag("verbose", type=bool, help="be verbose")
|
|
498
|
+
def run(target, count, verbose):
|
|
499
|
+
print(f"target={target} count={count} verbose={verbose}")
|
|
500
|
+
|
|
501
|
+
r = app.test(["run"])
|
|
502
|
+
assert r.exit_code == 0
|
|
503
|
+
assert "target=toml-value" in r.stdout
|
|
504
|
+
assert "count=42" in r.stdout
|
|
505
|
+
assert "verbose=True" in r.stdout
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def test_toml_format_set_writes_correctly(tmp_path):
|
|
509
|
+
"""TOML format config set writes valid TOML."""
|
|
510
|
+
config_file = tmp_path / "config.toml"
|
|
511
|
+
|
|
512
|
+
app = strictcli.App(
|
|
513
|
+
name="testapp",
|
|
514
|
+
version="1.0.0",
|
|
515
|
+
help="test app",
|
|
516
|
+
config=True,
|
|
517
|
+
config_path=str(config_file),
|
|
518
|
+
config_format="toml",
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
@app.command("run", help="run something")
|
|
522
|
+
@strictcli.flag("target", type=str, help="the target", default="default-val")
|
|
523
|
+
def run(target):
|
|
524
|
+
print(f"target={target}")
|
|
525
|
+
|
|
526
|
+
r = app.test(["config", "set", "target", "toml-written"])
|
|
527
|
+
assert r.exit_code == 0
|
|
528
|
+
assert config_file.exists()
|
|
529
|
+
|
|
530
|
+
import tomllib
|
|
531
|
+
with open(config_file, "rb") as f:
|
|
532
|
+
data = tomllib.load(f)
|
|
533
|
+
assert data["target"] == "toml-written"
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def test_toml_format_set_preserves_existing(tmp_path):
|
|
537
|
+
"""TOML format config set preserves existing keys."""
|
|
538
|
+
config_file = tmp_path / "config.toml"
|
|
539
|
+
config_file.write_text('existing = "keep-me"\n')
|
|
540
|
+
|
|
541
|
+
app = strictcli.App(
|
|
542
|
+
name="testapp",
|
|
543
|
+
version="1.0.0",
|
|
544
|
+
help="test app",
|
|
545
|
+
config=True,
|
|
546
|
+
config_path=str(config_file),
|
|
547
|
+
config_format="toml",
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
@app.command("run", help="run something")
|
|
551
|
+
@strictcli.flag("target", type=str, help="the target", default="default-val")
|
|
552
|
+
def run(target):
|
|
553
|
+
print(f"target={target}")
|
|
554
|
+
|
|
555
|
+
r = app.test(["config", "set", "target", "new-val"])
|
|
556
|
+
assert r.exit_code == 0
|
|
557
|
+
|
|
558
|
+
import tomllib
|
|
559
|
+
with open(config_file, "rb") as f:
|
|
560
|
+
data = tomllib.load(f)
|
|
561
|
+
assert data["target"] == "new-val"
|
|
562
|
+
assert data["existing"] == "keep-me"
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
def test_toml_format_config_path_command(tmp_path):
|
|
566
|
+
"""config path prints the custom path for TOML format."""
|
|
567
|
+
config_file = tmp_path / "my-config.toml"
|
|
568
|
+
config_file.write_text("")
|
|
569
|
+
|
|
570
|
+
app = strictcli.App(
|
|
571
|
+
name="testapp",
|
|
572
|
+
version="1.0.0",
|
|
573
|
+
help="test app",
|
|
574
|
+
config=True,
|
|
575
|
+
config_path=str(config_file),
|
|
576
|
+
config_format="toml",
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
@app.command("run", help="run something")
|
|
580
|
+
def run():
|
|
581
|
+
pass
|
|
582
|
+
|
|
583
|
+
r = app.test(["config", "path"])
|
|
584
|
+
assert r.exit_code == 0
|
|
585
|
+
assert str(config_file) in r.stdout
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
def test_toml_format_xdg_default_path(tmp_path, monkeypatch):
|
|
589
|
+
"""Without custom config_path, TOML format uses .toml extension in XDG path."""
|
|
590
|
+
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path))
|
|
591
|
+
|
|
592
|
+
app = strictcli.App(
|
|
593
|
+
name="testapp",
|
|
594
|
+
version="1.0.0",
|
|
595
|
+
help="test app",
|
|
596
|
+
config=True,
|
|
597
|
+
config_format="toml",
|
|
598
|
+
)
|
|
599
|
+
|
|
600
|
+
@app.command("run", help="run something")
|
|
601
|
+
def run():
|
|
602
|
+
pass
|
|
603
|
+
|
|
604
|
+
r = app.test(["config", "path"])
|
|
605
|
+
assert r.exit_code == 0
|
|
606
|
+
expected = os.path.join(str(tmp_path), "testapp", "config.toml")
|
|
607
|
+
assert expected in r.stdout
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
def test_invalid_toml_warning(tmp_path):
|
|
611
|
+
"""Invalid TOML file prints warning and falls back to defaults."""
|
|
612
|
+
config_file = tmp_path / "config.toml"
|
|
613
|
+
config_file.write_text("this is = not [ valid toml")
|
|
614
|
+
|
|
615
|
+
app = strictcli.App(
|
|
616
|
+
name="testapp",
|
|
617
|
+
version="1.0.0",
|
|
618
|
+
help="test app",
|
|
619
|
+
config=True,
|
|
620
|
+
config_path=str(config_file),
|
|
621
|
+
config_format="toml",
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
@app.command("run", help="run something")
|
|
625
|
+
@strictcli.flag("target", type=str, help="the target", default="default-val")
|
|
626
|
+
def run(target):
|
|
627
|
+
print(f"target={target}")
|
|
628
|
+
|
|
629
|
+
r = app.test(["run"])
|
|
630
|
+
assert r.exit_code == 0
|
|
631
|
+
assert "target=default-val" in r.stdout
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
def test_invalid_config_format():
|
|
635
|
+
"""Invalid config_format raises ValueError."""
|
|
636
|
+
with pytest.raises(ValueError, match='config_format must be'):
|
|
637
|
+
strictcli.App(
|
|
638
|
+
name="testapp",
|
|
639
|
+
version="1.0.0",
|
|
640
|
+
help="test app",
|
|
641
|
+
config=True,
|
|
642
|
+
config_format="yaml",
|
|
643
|
+
)
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
def test_default_json_unchanged(tmp_path, monkeypatch):
|
|
647
|
+
"""Default behavior (JSON, XDG path) is unchanged."""
|
|
648
|
+
config_home = _write_config(tmp_path, "testapp", {"target": "json-default"})
|
|
649
|
+
monkeypatch.setenv("XDG_CONFIG_HOME", config_home)
|
|
650
|
+
app = _make_config_app(config=True)
|
|
651
|
+
r = app.test(["run"])
|
|
652
|
+
assert r.exit_code == 0
|
|
653
|
+
assert "target=json-default" in r.stdout
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
def test_toml_config_show(tmp_path):
|
|
657
|
+
"""config show works with TOML format."""
|
|
658
|
+
config_file = tmp_path / "config.toml"
|
|
659
|
+
config_file.write_text('target = "toml-show-val"\n')
|
|
660
|
+
|
|
661
|
+
app = strictcli.App(
|
|
662
|
+
name="testapp",
|
|
663
|
+
version="1.0.0",
|
|
664
|
+
help="test app",
|
|
665
|
+
config=True,
|
|
666
|
+
config_path=str(config_file),
|
|
667
|
+
config_format="toml",
|
|
668
|
+
)
|
|
669
|
+
|
|
670
|
+
@app.command("run", help="run something")
|
|
671
|
+
@strictcli.flag("target", type=str, help="the target", default="default-val")
|
|
672
|
+
@strictcli.flag("count", type=int, help="how many", default=1)
|
|
673
|
+
def run(target, count):
|
|
674
|
+
pass
|
|
675
|
+
|
|
676
|
+
r = app.test(["config", "show"])
|
|
677
|
+
assert r.exit_code == 0
|
|
678
|
+
assert "target = toml-show-val (source: config)" in r.stdout
|
|
679
|
+
assert "count = 1 (source: default)" in r.stdout
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
def test_toml_float_value(tmp_path):
|
|
683
|
+
"""TOML config with float values works correctly."""
|
|
684
|
+
config_file = tmp_path / "config.toml"
|
|
685
|
+
config_file.write_text('ratio = 0.75\n')
|
|
686
|
+
|
|
687
|
+
app = strictcli.App(
|
|
688
|
+
name="testapp",
|
|
689
|
+
version="1.0.0",
|
|
690
|
+
help="test app",
|
|
691
|
+
config=True,
|
|
692
|
+
config_path=str(config_file),
|
|
693
|
+
config_format="toml",
|
|
694
|
+
)
|
|
695
|
+
|
|
696
|
+
@app.command("run", help="run something")
|
|
697
|
+
@strictcli.flag("ratio", type=float, help="ratio value", default=1.0)
|
|
698
|
+
def run(ratio):
|
|
699
|
+
print(f"ratio={ratio}")
|
|
700
|
+
|
|
701
|
+
r = app.test(["run"])
|
|
702
|
+
assert r.exit_code == 0
|
|
703
|
+
assert "ratio=0.75" in r.stdout
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
cb4155e0427b58ad57b6684d038a8ec780101d5b
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|