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.
Files changed (113) hide show
  1. strictcli-0.9.0/.rlsbl/changes/.validated +1 -0
  2. strictcli-0.9.0/.rlsbl/changes/0.9.0.jsonl +5 -0
  3. strictcli-0.9.0/.rlsbl/changes/0.9.0.md +5 -0
  4. strictcli-0.9.0/.rlsbl/releases/v0.9.0.toml +3 -0
  5. {strictcli-0.8.7 → strictcli-0.9.0}/CHANGELOG.md +6 -0
  6. {strictcli-0.8.7 → strictcli-0.9.0}/PKG-INFO +1 -1
  7. {strictcli-0.8.7 → strictcli-0.9.0}/package-lock.json +2 -2
  8. {strictcli-0.8.7 → strictcli-0.9.0}/package.json +1 -1
  9. {strictcli-0.8.7 → strictcli-0.9.0}/pyproject.toml +1 -1
  10. {strictcli-0.8.7 → strictcli-0.9.0}/strictcli/__init__.py +106 -23
  11. {strictcli-0.8.7 → strictcli-0.9.0}/tests/test_config.py +325 -0
  12. {strictcli-0.8.7 → strictcli-0.9.0}/uv.lock +1 -1
  13. strictcli-0.8.7/.rlsbl/changes/.validated +0 -1
  14. {strictcli-0.8.7 → strictcli-0.9.0}/.claude/settings.json +0 -0
  15. {strictcli-0.8.7 → strictcli-0.9.0}/.github/workflows/ci.yml +0 -0
  16. {strictcli-0.8.7 → strictcli-0.9.0}/.github/workflows/publish.yml +0 -0
  17. {strictcli-0.8.7 → strictcli-0.9.0}/.gitignore +0 -0
  18. {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/bases/.claude/settings.json +0 -0
  19. {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/bases/.github/workflows/ci.yml +0 -0
  20. {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/bases/.github/workflows/publish.yml +0 -0
  21. {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/bases/.gitignore +0 -0
  22. {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/bases/.rlsbl/hooks/post-release.sh +0 -0
  23. {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/bases/.rlsbl/hooks/pre-checks.sh +0 -0
  24. {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/bases/.rlsbl/hooks/pre-release.sh +0 -0
  25. {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/bases/.rlsbl/lint/go.toml +0 -0
  26. {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/bases/.rlsbl/lint/npm.toml +0 -0
  27. {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/bases/.rlsbl/lint/python.toml +0 -0
  28. {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/bases/CHANGELOG.md +0 -0
  29. {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/bases/CLAUDE.md +0 -0
  30. {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/bases/LICENSE +0 -0
  31. {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/changes/0.4.0.jsonl +0 -0
  32. {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/changes/0.4.0.md +0 -0
  33. {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/changes/0.4.1.jsonl +0 -0
  34. {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/changes/0.4.1.md +0 -0
  35. {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/changes/0.5.0.jsonl +0 -0
  36. {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/changes/0.5.0.md +0 -0
  37. {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/changes/0.6.0.jsonl +0 -0
  38. {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/changes/0.6.0.md +0 -0
  39. {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/changes/0.6.1.jsonl +0 -0
  40. {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/changes/0.6.1.md +0 -0
  41. {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/changes/0.7.0.jsonl +0 -0
  42. {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/changes/0.7.0.md +0 -0
  43. {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/changes/0.7.1.jsonl +0 -0
  44. {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/changes/0.7.1.md +0 -0
  45. {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/changes/0.8.0.jsonl +0 -0
  46. {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/changes/0.8.0.md +0 -0
  47. {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/changes/0.8.1.jsonl +0 -0
  48. {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/changes/0.8.1.md +0 -0
  49. {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/changes/0.8.2.jsonl +0 -0
  50. {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/changes/0.8.2.md +0 -0
  51. {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/changes/0.8.3.jsonl +0 -0
  52. {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/changes/0.8.3.md +0 -0
  53. {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/changes/0.8.4.jsonl +0 -0
  54. {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/changes/0.8.4.md +0 -0
  55. {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/changes/0.8.5.jsonl +0 -0
  56. {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/changes/0.8.5.md +0 -0
  57. {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/changes/0.8.6.jsonl +0 -0
  58. {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/changes/0.8.6.md +0 -0
  59. {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/changes/0.8.7.jsonl +0 -0
  60. {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/changes/0.8.7.md +0 -0
  61. {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/changes/unreleased.jsonl +0 -0
  62. {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/config.json +0 -0
  63. {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/hashes.json +0 -0
  64. {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/hooks/post-release.sh +0 -0
  65. {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/hooks/pre-checks.sh +0 -0
  66. {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/hooks/pre-release.sh +0 -0
  67. {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/lint/go.toml +0 -0
  68. {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/lint/npm.toml +0 -0
  69. {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/lint/python.toml +0 -0
  70. {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/releases/unreleased.toml +0 -0
  71. {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/releases/v0.8.5.toml +0 -0
  72. {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/releases/v0.8.6.toml +0 -0
  73. {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/releases/v0.8.7.toml +0 -0
  74. {strictcli-0.8.7 → strictcli-0.9.0}/.rlsbl/version +0 -0
  75. {strictcli-0.8.7 → strictcli-0.9.0}/.strictcli/schema.json +0 -0
  76. {strictcli-0.8.7 → strictcli-0.9.0}/CLAUDE.md +0 -0
  77. {strictcli-0.8.7 → strictcli-0.9.0}/LICENSE +0 -0
  78. {strictcli-0.8.7 → strictcli-0.9.0}/README.md +0 -0
  79. {strictcli-0.8.7 → strictcli-0.9.0}/index.js +0 -0
  80. {strictcli-0.8.7 → strictcli-0.9.0}/postinstall.js +0 -0
  81. {strictcli-0.8.7 → strictcli-0.9.0}/tests/test_arg_default.py +0 -0
  82. {strictcli-0.8.7 → strictcli-0.9.0}/tests/test_auto_version.py +0 -0
  83. {strictcli-0.8.7 → strictcli-0.9.0}/tests/test_check_command.py +0 -0
  84. {strictcli-0.8.7 → strictcli-0.9.0}/tests/test_check_discovery.py +0 -0
  85. {strictcli-0.8.7 → strictcli-0.9.0}/tests/test_check_runner.py +0 -0
  86. {strictcli-0.8.7 → strictcli-0.9.0}/tests/test_check_schema.py +0 -0
  87. {strictcli-0.8.7 → strictcli-0.9.0}/tests/test_check_types.py +0 -0
  88. {strictcli-0.8.7 → strictcli-0.9.0}/tests/test_choices.py +0 -0
  89. {strictcli-0.8.7 → strictcli-0.9.0}/tests/test_command_help_suggestion.py +0 -0
  90. {strictcli-0.8.7 → strictcli-0.9.0}/tests/test_deep_nesting.py +0 -0
  91. {strictcli-0.8.7 → strictcli-0.9.0}/tests/test_dependencies.py +0 -0
  92. {strictcli-0.8.7 → strictcli-0.9.0}/tests/test_deprecated.py +0 -0
  93. {strictcli-0.8.7 → strictcli-0.9.0}/tests/test_dump_schema.py +0 -0
  94. {strictcli-0.8.7 → strictcli-0.9.0}/tests/test_e2e.py +0 -0
  95. {strictcli-0.8.7 → strictcli-0.9.0}/tests/test_env.py +0 -0
  96. {strictcli-0.8.7 → strictcli-0.9.0}/tests/test_exit_codes.py +0 -0
  97. {strictcli-0.8.7 → strictcli-0.9.0}/tests/test_float_type.py +0 -0
  98. {strictcli-0.8.7 → strictcli-0.9.0}/tests/test_global_flags.py +0 -0
  99. {strictcli-0.8.7 → strictcli-0.9.0}/tests/test_help.py +0 -0
  100. {strictcli-0.8.7 → strictcli-0.9.0}/tests/test_int_type.py +0 -0
  101. {strictcli-0.8.7 → strictcli-0.9.0}/tests/test_mutex.py +0 -0
  102. {strictcli-0.8.7 → strictcli-0.9.0}/tests/test_nesting.py +0 -0
  103. {strictcli-0.8.7 → strictcli-0.9.0}/tests/test_parser.py +0 -0
  104. {strictcli-0.8.7 → strictcli-0.9.0}/tests/test_passthrough.py +0 -0
  105. {strictcli-0.8.7 → strictcli-0.9.0}/tests/test_registration.py +0 -0
  106. {strictcli-0.8.7 → strictcli-0.9.0}/tests/test_repeatable.py +0 -0
  107. {strictcli-0.8.7 → strictcli-0.9.0}/tests/test_tagdsl.py +0 -0
  108. {strictcli-0.8.7 → strictcli-0.9.0}/tests/test_tags.py +0 -0
  109. {strictcli-0.8.7 → strictcli-0.9.0}/tests/test_toml_loading.py +0 -0
  110. {strictcli-0.8.7 → strictcli-0.9.0}/tests/test_validate.py +0 -0
  111. {strictcli-0.8.7 → strictcli-0.9.0}/tests/test_variadic.py +0 -0
  112. {strictcli-0.8.7 → strictcli-0.9.0}/todo/.defer/deferred.md +0 -0
  113. {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}
@@ -0,0 +1,5 @@
1
+ ## 0.9.0
2
+
3
+ ### Features
4
+
5
+ - **New feature.** config_path and config_format options for TOML-based configuration file support.
@@ -0,0 +1,3 @@
1
+ bump = "minor"
2
+ include = ["pypi", "npm"]
3
+ exclude = []
@@ -2,6 +2,12 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ## 0.9.0
6
+
7
+ ### Features
8
+
9
+ - **New feature.** config_path and config_format options for TOML-based configuration file support.
10
+
5
11
  ## 0.8.7
6
12
 
7
13
  ### Features
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: strictcli
3
- Version: 0.8.7
3
+ Version: 0.9.0
4
4
  Summary: A strict, zero-dependency CLI framework for Python
5
5
  Project-URL: Homepage, https://github.com/smm-h/strictcli
6
6
  Project-URL: Repository, https://github.com/smm-h/strictcli
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "strictcli",
3
- "version": "0.8.7",
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.8.7",
9
+ "version": "0.9.0",
10
10
  "hasInstallScript": true,
11
11
  "license": "MIT"
12
12
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "strictcli",
3
- "version": "0.8.7",
3
+ "version": "0.9.0",
4
4
  "description": "A strict, zero-dependency CLI framework for Python (npm wrapper)",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "strictcli"
7
- version = "0.8.7"
7
+ version = "0.9.0"
8
8
  description = "A strict, zero-dependency CLI framework for Python"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- __version__ = "0.8.7"
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
- return os.path.join(config_home, app_name, "config.json")
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(app_name: str) -> dict:
46
- """Load the JSON config file for an app.
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 JSON.
49
- Invalid JSON prints a warning to stderr.
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(self.name)
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(app_ref.name)),
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(app_ref.name)
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(app_ref.name)
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
- try:
882
- with open(path) as fh:
883
- existing = json.loads(fh.read())
884
- except (json.JSONDecodeError, ValueError):
885
- existing = {}
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
- with open(path, "w") as fh:
888
- fh.write(json.dumps(existing, indent=2) + "\n")
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(app_ref.name)
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
- with open(path, "w") as fh:
907
- fh.write("{}\n")
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
@@ -232,7 +232,7 @@ wheels = [
232
232
 
233
233
  [[package]]
234
234
  name = "strictcli"
235
- version = "0.8.7"
235
+ version = "0.9.0"
236
236
  source = { editable = "." }
237
237
 
238
238
  [package.dev-dependencies]
@@ -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