strictcli 0.8.7__tar.gz → 0.9.1__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.1/.rlsbl/changes/.validated +1 -0
- strictcli-0.9.1/.rlsbl/changes/0.9.0.jsonl +5 -0
- strictcli-0.9.1/.rlsbl/changes/0.9.0.md +5 -0
- strictcli-0.9.1/.rlsbl/changes/0.9.1.jsonl +9 -0
- strictcli-0.9.1/.rlsbl/changes/0.9.1.md +9 -0
- strictcli-0.9.1/.rlsbl/releases/v0.9.0.toml +3 -0
- strictcli-0.9.1/.rlsbl/releases/v0.9.1.toml +3 -0
- strictcli-0.9.1/.rlsbl/version +1 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/CHANGELOG.md +16 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/PKG-INFO +1 -1
- {strictcli-0.8.7 → strictcli-0.9.1}/package-lock.json +2 -2
- {strictcli-0.8.7 → strictcli-0.9.1}/package.json +1 -1
- {strictcli-0.8.7 → strictcli-0.9.1}/pyproject.toml +1 -1
- {strictcli-0.8.7 → strictcli-0.9.1}/strictcli/__init__.py +111 -23
- {strictcli-0.8.7 → strictcli-0.9.1}/tests/test_config.py +325 -0
- strictcli-0.9.1/tests/test_config_file_path.py +60 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/uv.lock +1 -1
- strictcli-0.8.7/.rlsbl/changes/.validated +0 -1
- strictcli-0.8.7/.rlsbl/version +0 -1
- {strictcli-0.8.7 → strictcli-0.9.1}/.claude/settings.json +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/.github/workflows/ci.yml +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/.github/workflows/publish.yml +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/.gitignore +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/.rlsbl/bases/.claude/settings.json +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/.rlsbl/bases/.github/workflows/ci.yml +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/.rlsbl/bases/.github/workflows/publish.yml +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/.rlsbl/bases/.gitignore +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/.rlsbl/bases/.rlsbl/hooks/post-release.sh +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/.rlsbl/bases/.rlsbl/hooks/pre-checks.sh +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/.rlsbl/bases/.rlsbl/hooks/pre-release.sh +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/.rlsbl/bases/.rlsbl/lint/go.toml +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/.rlsbl/bases/.rlsbl/lint/npm.toml +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/.rlsbl/bases/.rlsbl/lint/python.toml +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/.rlsbl/bases/CHANGELOG.md +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/.rlsbl/bases/CLAUDE.md +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/.rlsbl/bases/LICENSE +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/.rlsbl/changes/0.4.0.jsonl +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/.rlsbl/changes/0.4.0.md +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/.rlsbl/changes/0.4.1.jsonl +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/.rlsbl/changes/0.4.1.md +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/.rlsbl/changes/0.5.0.jsonl +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/.rlsbl/changes/0.5.0.md +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/.rlsbl/changes/0.6.0.jsonl +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/.rlsbl/changes/0.6.0.md +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/.rlsbl/changes/0.6.1.jsonl +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/.rlsbl/changes/0.6.1.md +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/.rlsbl/changes/0.7.0.jsonl +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/.rlsbl/changes/0.7.0.md +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/.rlsbl/changes/0.7.1.jsonl +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/.rlsbl/changes/0.7.1.md +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/.rlsbl/changes/0.8.0.jsonl +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/.rlsbl/changes/0.8.0.md +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/.rlsbl/changes/0.8.1.jsonl +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/.rlsbl/changes/0.8.1.md +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/.rlsbl/changes/0.8.2.jsonl +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/.rlsbl/changes/0.8.2.md +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/.rlsbl/changes/0.8.3.jsonl +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/.rlsbl/changes/0.8.3.md +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/.rlsbl/changes/0.8.4.jsonl +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/.rlsbl/changes/0.8.4.md +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/.rlsbl/changes/0.8.5.jsonl +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/.rlsbl/changes/0.8.5.md +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/.rlsbl/changes/0.8.6.jsonl +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/.rlsbl/changes/0.8.6.md +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/.rlsbl/changes/0.8.7.jsonl +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/.rlsbl/changes/0.8.7.md +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/.rlsbl/changes/unreleased.jsonl +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/.rlsbl/config.json +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/.rlsbl/hashes.json +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/.rlsbl/hooks/post-release.sh +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/.rlsbl/hooks/pre-checks.sh +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/.rlsbl/hooks/pre-release.sh +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/.rlsbl/lint/go.toml +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/.rlsbl/lint/npm.toml +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/.rlsbl/lint/python.toml +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/.rlsbl/releases/unreleased.toml +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/.rlsbl/releases/v0.8.5.toml +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/.rlsbl/releases/v0.8.6.toml +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/.rlsbl/releases/v0.8.7.toml +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/.strictcli/schema.json +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/CLAUDE.md +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/LICENSE +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/README.md +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/index.js +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/postinstall.js +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/tests/test_arg_default.py +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/tests/test_auto_version.py +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/tests/test_check_command.py +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/tests/test_check_discovery.py +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/tests/test_check_runner.py +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/tests/test_check_schema.py +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/tests/test_check_types.py +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/tests/test_choices.py +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/tests/test_command_help_suggestion.py +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/tests/test_deep_nesting.py +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/tests/test_dependencies.py +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/tests/test_deprecated.py +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/tests/test_dump_schema.py +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/tests/test_e2e.py +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/tests/test_env.py +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/tests/test_exit_codes.py +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/tests/test_float_type.py +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/tests/test_global_flags.py +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/tests/test_help.py +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/tests/test_int_type.py +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/tests/test_mutex.py +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/tests/test_nesting.py +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/tests/test_parser.py +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/tests/test_passthrough.py +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/tests/test_registration.py +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/tests/test_repeatable.py +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/tests/test_tagdsl.py +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/tests/test_tags.py +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/tests/test_toml_loading.py +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/tests/test_validate.py +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/tests/test_variadic.py +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/todo/.defer/deferred.md +0 -0
- {strictcli-0.8.7 → strictcli-0.9.1}/todo/.done/original-idea.md +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
4a6d209ab6bd677356d90300dcb5d073d2d73935
|
|
@@ -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}
|
|
@@ -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.41.5
|
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
# Changelog
|
|
4
4
|
|
|
5
|
+
## 0.9.1
|
|
6
|
+
|
|
7
|
+
### Features
|
|
8
|
+
|
|
9
|
+
- **New feature.** Public `config_file_path` property on App.
|
|
10
|
+
|
|
11
|
+
### Fixes
|
|
12
|
+
|
|
13
|
+
- **Fix.** Config format validation errors now have parity between Python and Go.
|
|
14
|
+
|
|
15
|
+
## 0.9.0
|
|
16
|
+
|
|
17
|
+
### Features
|
|
18
|
+
|
|
19
|
+
- **New feature.** config_path and config_format options for TOML-based configuration file support.
|
|
20
|
+
|
|
5
21
|
## 0.8.7
|
|
6
22
|
|
|
7
23
|
### Features
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "strictcli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "strictcli",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.9.1",
|
|
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.1"
|
|
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
|
|
@@ -616,6 +669,11 @@ class App:
|
|
|
616
669
|
self._check_defs = {}
|
|
617
670
|
self._checks_enabled = False
|
|
618
671
|
|
|
672
|
+
@property
|
|
673
|
+
def config_file_path(self) -> str:
|
|
674
|
+
"""Return the resolved config file path for this app."""
|
|
675
|
+
return _config_path(self.name, override=self.config_path, config_format=self.config_format)
|
|
676
|
+
|
|
619
677
|
def check(self, name: str):
|
|
620
678
|
"""Decorator to register a check implementation."""
|
|
621
679
|
def decorator(fn):
|
|
@@ -843,12 +901,20 @@ class App:
|
|
|
843
901
|
config_grp.commands["path"] = Command(
|
|
844
902
|
name="path",
|
|
845
903
|
help="Print the config file path",
|
|
846
|
-
handler=lambda **_kw: print(_config_path(
|
|
904
|
+
handler=lambda **_kw: print(_config_path(
|
|
905
|
+
app_ref.name,
|
|
906
|
+
override=app_ref.config_path,
|
|
907
|
+
config_format=app_ref.config_format,
|
|
908
|
+
)),
|
|
847
909
|
)
|
|
848
910
|
|
|
849
911
|
# config show
|
|
850
912
|
def _config_show_handler(**_kw) -> None:
|
|
851
|
-
config_data = _load_config(
|
|
913
|
+
config_data = _load_config(
|
|
914
|
+
app_ref.name,
|
|
915
|
+
config_path_override=app_ref.config_path,
|
|
916
|
+
config_format=app_ref.config_format,
|
|
917
|
+
)
|
|
852
918
|
all_flags = app_ref._collect_all_flags()
|
|
853
919
|
for f in all_flags:
|
|
854
920
|
param = _flag_param_name(f.name)
|
|
@@ -872,20 +938,34 @@ class App:
|
|
|
872
938
|
|
|
873
939
|
# config set
|
|
874
940
|
def _config_set_handler(key, value, **_kw) -> None:
|
|
875
|
-
path = _config_path(
|
|
941
|
+
path = _config_path(
|
|
942
|
+
app_ref.name,
|
|
943
|
+
override=app_ref.config_path,
|
|
944
|
+
config_format=app_ref.config_format,
|
|
945
|
+
)
|
|
876
946
|
dir_path = os.path.dirname(path)
|
|
877
947
|
os.makedirs(dir_path, exist_ok=True)
|
|
878
948
|
# Read existing config
|
|
879
949
|
existing: dict = {}
|
|
880
950
|
if os.path.isfile(path):
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
951
|
+
if app_ref.config_format == "toml":
|
|
952
|
+
try:
|
|
953
|
+
with open(path, "rb") as fh:
|
|
954
|
+
existing = tomllib.load(fh)
|
|
955
|
+
except (tomllib.TOMLDecodeError, UnicodeDecodeError):
|
|
956
|
+
existing = {}
|
|
957
|
+
else:
|
|
958
|
+
try:
|
|
959
|
+
with open(path) as fh:
|
|
960
|
+
existing = json.loads(fh.read())
|
|
961
|
+
except (json.JSONDecodeError, ValueError):
|
|
962
|
+
existing = {}
|
|
886
963
|
existing[key] = value
|
|
887
|
-
|
|
888
|
-
|
|
964
|
+
if app_ref.config_format == "toml":
|
|
965
|
+
_write_toml_flat(existing, path)
|
|
966
|
+
else:
|
|
967
|
+
with open(path, "w") as fh:
|
|
968
|
+
fh.write(json.dumps(existing, indent=2) + "\n")
|
|
889
969
|
|
|
890
970
|
config_grp.commands["set"] = Command(
|
|
891
971
|
name="set",
|
|
@@ -899,12 +979,20 @@ class App:
|
|
|
899
979
|
|
|
900
980
|
# config edit
|
|
901
981
|
def _config_edit_handler(**_kw) -> None:
|
|
902
|
-
path = _config_path(
|
|
982
|
+
path = _config_path(
|
|
983
|
+
app_ref.name,
|
|
984
|
+
override=app_ref.config_path,
|
|
985
|
+
config_format=app_ref.config_format,
|
|
986
|
+
)
|
|
903
987
|
dir_path = os.path.dirname(path)
|
|
904
988
|
os.makedirs(dir_path, exist_ok=True)
|
|
905
989
|
if not os.path.isfile(path):
|
|
906
|
-
|
|
907
|
-
|
|
990
|
+
if app_ref.config_format == "toml":
|
|
991
|
+
with open(path, "w") as fh:
|
|
992
|
+
fh.write("")
|
|
993
|
+
else:
|
|
994
|
+
with open(path, "w") as fh:
|
|
995
|
+
fh.write("{}\n")
|
|
908
996
|
editor = os.environ.get("EDITOR", "vi")
|
|
909
997
|
subprocess.run([editor, path])
|
|
910
998
|
|
|
@@ -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
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Tests for App.config_file_path property."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
import strictcli
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_default_xdg_path_json(tmp_path, monkeypatch):
|
|
9
|
+
"""Default config_file_path uses XDG_CONFIG_HOME with .json extension."""
|
|
10
|
+
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path))
|
|
11
|
+
app = strictcli.App(name="testapp", version="1.0.0", help="test app")
|
|
12
|
+
expected = os.path.join(str(tmp_path), "testapp", "config.json")
|
|
13
|
+
assert app.config_file_path == expected
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_default_xdg_path_toml(tmp_path, monkeypatch):
|
|
17
|
+
"""config_file_path uses .toml extension when config_format='toml'."""
|
|
18
|
+
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path))
|
|
19
|
+
app = strictcli.App(
|
|
20
|
+
name="testapp", version="1.0.0", help="test app", config_format="toml",
|
|
21
|
+
)
|
|
22
|
+
expected = os.path.join(str(tmp_path), "testapp", "config.toml")
|
|
23
|
+
assert app.config_file_path == expected
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_custom_config_path_override(tmp_path):
|
|
27
|
+
"""config_file_path returns the override path when config_path is set."""
|
|
28
|
+
custom = str(tmp_path / "custom" / "settings.json")
|
|
29
|
+
app = strictcli.App(
|
|
30
|
+
name="testapp", version="1.0.0", help="test app",
|
|
31
|
+
config_path=custom,
|
|
32
|
+
)
|
|
33
|
+
assert app.config_file_path == custom
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_tilde_expansion(tmp_path, monkeypatch):
|
|
37
|
+
"""config_file_path expands ~ in config_path."""
|
|
38
|
+
monkeypatch.setenv("HOME", str(tmp_path))
|
|
39
|
+
app = strictcli.App(
|
|
40
|
+
name="testapp", version="1.0.0", help="test app",
|
|
41
|
+
config_path="~/.myapp/config.json",
|
|
42
|
+
)
|
|
43
|
+
expected = os.path.join(str(tmp_path), ".myapp", "config.json")
|
|
44
|
+
assert app.config_file_path == expected
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_matches_config_path_command(tmp_path, monkeypatch):
|
|
48
|
+
"""config_file_path matches what 'config path' command prints."""
|
|
49
|
+
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path))
|
|
50
|
+
app = strictcli.App(
|
|
51
|
+
name="testapp", version="1.0.0", help="test app", config=True,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
@app.command("run", help="run something")
|
|
55
|
+
def run():
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
r = app.test(["config", "path"])
|
|
59
|
+
assert r.exit_code == 0
|
|
60
|
+
assert app.config_file_path in r.stdout
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
cb4155e0427b58ad57b6684d038a8ec780101d5b
|
strictcli-0.8.7/.rlsbl/version
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
0.41.4
|
|
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
|