strictcli 0.32.1__tar.gz → 0.32.2__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 (87) hide show
  1. {strictcli-0.32.1 → strictcli-0.32.2}/PKG-INFO +1 -1
  2. {strictcli-0.32.1 → strictcli-0.32.2}/pyproject.toml +1 -1
  3. {strictcli-0.32.1 → strictcli-0.32.2}/strictcli/__init__.py +18 -9
  4. {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_coverage.py +3 -0
  5. {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_float_type.py +36 -0
  6. {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_int_type.py +25 -0
  7. {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_typed_args.py +34 -0
  8. {strictcli-0.32.1 → strictcli-0.32.2}/uv.lock +1 -1
  9. {strictcli-0.32.1 → strictcli-0.32.2}/.claude/settings.json +0 -0
  10. {strictcli-0.32.1 → strictcli-0.32.2}/.github/workflows/ci.yml +0 -0
  11. {strictcli-0.32.1 → strictcli-0.32.2}/.github/workflows/publish.yml +0 -0
  12. {strictcli-0.32.1 → strictcli-0.32.2}/.gitignore +0 -0
  13. {strictcli-0.32.1 → strictcli-0.32.2}/.rlsbl/bases/.github/workflows/ci.yml +0 -0
  14. {strictcli-0.32.1 → strictcli-0.32.2}/.rlsbl/bases/.github/workflows/publish.yml +0 -0
  15. {strictcli-0.32.1 → strictcli-0.32.2}/.rlsbl/bases/.gitignore +0 -0
  16. {strictcli-0.32.1 → strictcli-0.32.2}/.rlsbl/bases/.rlsbl/lint/python.toml +0 -0
  17. {strictcli-0.32.1 → strictcli-0.32.2}/.rlsbl/config.json +0 -0
  18. {strictcli-0.32.1 → strictcli-0.32.2}/.rlsbl/lint/python.toml +0 -0
  19. {strictcli-0.32.1 → strictcli-0.32.2}/.rlsbl/managed-files.json +0 -0
  20. {strictcli-0.32.1 → strictcli-0.32.2}/.rlsbl/version +0 -0
  21. {strictcli-0.32.1 → strictcli-0.32.2}/.strictcli/schema.json +0 -0
  22. {strictcli-0.32.1 → strictcli-0.32.2}/CLAUDE.md +0 -0
  23. {strictcli-0.32.1 → strictcli-0.32.2}/LICENSE +0 -0
  24. {strictcli-0.32.1 → strictcli-0.32.2}/README.md +0 -0
  25. {strictcli-0.32.1 → strictcli-0.32.2}/strictcli/py.typed +0 -0
  26. {strictcli-0.32.1 → strictcli-0.32.2}/tests/conftest.py +0 -0
  27. {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_arg_default.py +0 -0
  28. {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_arg_default_validation.py +0 -0
  29. {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_at_prefix.py +0 -0
  30. {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_auto_version.py +0 -0
  31. {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_call.py +0 -0
  32. {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_check_command.py +0 -0
  33. {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_check_discovery.py +0 -0
  34. {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_check_provider.py +0 -0
  35. {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_check_public_api.py +0 -0
  36. {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_check_runner.py +0 -0
  37. {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_check_schema.py +0 -0
  38. {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_check_types.py +0 -0
  39. {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_choices.py +0 -0
  40. {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_choices_none.py +0 -0
  41. {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_command_help_suggestion.py +0 -0
  42. {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_command_tags.py +0 -0
  43. {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_compound_types.py +0 -0
  44. {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_config.py +0 -0
  45. {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_config_fields.py +0 -0
  46. {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_config_file_path.py +0 -0
  47. {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_config_set_bugs.py +0 -0
  48. {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_context.py +0 -0
  49. {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_deep_nesting.py +0 -0
  50. {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_dependencies.py +0 -0
  51. {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_deprecated.py +0 -0
  52. {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_dump_schema.py +0 -0
  53. {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_e2e.py +0 -0
  54. {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_env.py +0 -0
  55. {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_exit_codes.py +0 -0
  56. {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_flag_sets.py +0 -0
  57. {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_float_format.py +0 -0
  58. {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_float_vectors.py +0 -0
  59. {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_global_flag_conflict_position.py +0 -0
  60. {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_global_flags.py +0 -0
  61. {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_help.py +0 -0
  62. {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_hermetic.py +0 -0
  63. {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_infra_env.py +0 -0
  64. {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_invoke.py +0 -0
  65. {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_keyword_flags.py +0 -0
  66. {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_mcp.py +0 -0
  67. {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_mutex.py +0 -0
  68. {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_nesting.py +0 -0
  69. {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_parser.py +0 -0
  70. {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_passthrough.py +0 -0
  71. {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_provenance.py +0 -0
  72. {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_provenance_phase2.py +0 -0
  73. {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_registration.py +0 -0
  74. {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_repeatable.py +0 -0
  75. {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_reserved_global_flags.py +0 -0
  76. {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_tagdsl.py +0 -0
  77. {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_toml_loading.py +0 -0
  78. {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_tool_export.py +0 -0
  79. {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_unique.py +0 -0
  80. {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_utilities.py +0 -0
  81. {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_validate.py +0 -0
  82. {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_variadic.py +0 -0
  83. {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_visibility.py +0 -0
  84. {strictcli-0.32.1 → strictcli-0.32.2}/todo/.defer/deferred.md +0 -0
  85. {strictcli-0.32.1 → strictcli-0.32.2}/todo/.done/keyword-collision-in-flag-param-name.md +0 -0
  86. {strictcli-0.32.1 → strictcli-0.32.2}/todo/.done/original-idea.md +0 -0
  87. {strictcli-0.32.1 → strictcli-0.32.2}/todo/.done/public-check-runner-api.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: strictcli
3
- Version: 0.32.1
3
+ Version: 0.32.2
4
4
  Summary: A strict 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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "strictcli"
7
- version = "0.32.1"
7
+ version = "0.32.2"
8
8
  description = "A strict 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.32.1"
5
+ __version__ = "0.32.2"
6
6
 
7
7
  __all__ = [
8
8
  "App", "Flag", "Arg", "FlagSet", "MutexGroup", "CoRequired", "Requires",
@@ -1341,6 +1341,10 @@ def _strict_int(s: str) -> int:
1341
1341
  """
1342
1342
  if s != s.strip():
1343
1343
  raise ValueError(f"expected integer, got '{s}'")
1344
+ # Python's int() accepts PEP 515 underscore digit separators ('1_000');
1345
+ # Go's strconv and the TypeScript parser reject them. Reject to match canon.
1346
+ if "_" in s:
1347
+ raise ValueError(f"expected integer, got '{s}'")
1344
1348
  try:
1345
1349
  n = int(s)
1346
1350
  except ValueError:
@@ -1363,7 +1367,13 @@ def _strict_float(s: str) -> float:
1363
1367
  raise ValueError("NaN is not allowed")
1364
1368
  if low in ("inf", "-inf", "+inf", "infinity", "-infinity", "+infinity"):
1365
1369
  raise ValueError("Inf is not allowed")
1366
- return float(s)
1370
+ result = float(s)
1371
+ # A finite literal that overflows to +/-inf ('1e999') is a parse failure,
1372
+ # not an Inf literal. Go and TypeScript reject it as an invalid float; use
1373
+ # the generic "expected float" path (not "Inf is not allowed") to match.
1374
+ if math.isinf(result):
1375
+ raise ValueError(f"invalid literal for float(): {s!r}")
1376
+ return result
1367
1377
 
1368
1378
 
1369
1379
  def _float_parse_error(
@@ -2801,7 +2811,6 @@ class App:
2801
2811
  # Test-coverage instrumentation. When enabled, every test() and call()
2802
2812
  # invocation records which command was dispatched so a check can verify
2803
2813
  # that every command in the app's surface has been exercised.
2804
- self._coverage_shard_counter: int = 0
2805
2814
  self._coverage_shard_path: str | None = None
2806
2815
  self._coverage_dir: str | None = None
2807
2816
  self._coverage_manifest_path: str | None = None
@@ -2819,7 +2828,7 @@ class App:
2819
2828
  )
2820
2829
  self._coverage_shard_path = os.path.join(
2821
2830
  self._coverage_dir,
2822
- f"{os.getpid()}-{{n}}.jsonl",
2831
+ f"{os.getpid()}.jsonl",
2823
2832
  )
2824
2833
  os.makedirs(self._coverage_dir, exist_ok=True)
2825
2834
  self.register_check_provider(self._test_coverage_provider)
@@ -2847,14 +2856,14 @@ class App:
2847
2856
  def _record_coverage(self, cmd_path: str) -> None:
2848
2857
  """Append a coverage record for the resolved command path.
2849
2858
 
2850
- Each test() or call() invocation writes one JSONL line to a per-process
2851
- shard file. The shard counter increments on each write to keep files
2852
- small and unique (handles pytest-xdist where multiple workers share a
2853
- PID namespace within the coverage directory).
2859
+ Each test() or call() invocation appends one JSONL line to the
2860
+ process's shard file (named "<pid>.jsonl"). Uniqueness across concurrent
2861
+ writers comes from the PID and O_APPEND; one shard per process is
2862
+ sufficient, so there is no per-write shard counter.
2854
2863
  """
2855
2864
  if self._coverage_shard_path is None:
2856
2865
  return
2857
- path = self._coverage_shard_path.format(n=self._coverage_shard_counter)
2866
+ path = self._coverage_shard_path
2858
2867
  os.makedirs(os.path.dirname(path), exist_ok=True)
2859
2868
  with open(path, "a", encoding="utf-8") as f:
2860
2869
  f.write(json.dumps({"command": cmd_path}) + "\n")
@@ -84,6 +84,9 @@ class TestCoverageRecording:
84
84
  shards = list(coverage_dir.glob("*.jsonl"))
85
85
  assert len(shards) >= 1
86
86
 
87
+ # One shard per process, named "<pid>.jsonl" (no shard counter suffix).
88
+ assert shards[0].name == f"{os.getpid()}.jsonl"
89
+
87
90
  entries = []
88
91
  for shard in shards:
89
92
  for line in shard.read_text().strip().splitlines():
@@ -158,6 +158,42 @@ def test_float_flag_reject_negative_inf():
158
158
  assert "Inf is not allowed" in r.stderr
159
159
 
160
160
 
161
+ def test_float_flag_reject_overflow_to_infinity():
162
+ """'1e999' overflows to inf and is rejected via the generic parse error."""
163
+ app = _make_app_with_float_flag()
164
+ r = app.test(["cmd", "--rate", "1e999"])
165
+ assert r.exit_code == 1
166
+ assert "expected float" in r.stderr
167
+ assert "'1e999'" in r.stderr
168
+ # Overflow uses the generic message, not the literal-Inf rejection.
169
+ assert "Inf is not allowed" not in r.stderr
170
+
171
+
172
+ def test_float_flag_reject_negative_overflow_to_infinity():
173
+ """'-1e999' overflows to -inf and is rejected via the generic parse error."""
174
+ app = _make_app_with_float_flag()
175
+ r = app.test(["cmd", "--rate", "-1e999"])
176
+ assert r.exit_code == 1
177
+ assert "expected float" in r.stderr
178
+ assert "'-1e999'" in r.stderr
179
+
180
+
181
+ def test_float_flag_reject_overflow_env_value(monkeypatch):
182
+ """Overflowing float env var is rejected with the env suffix."""
183
+ app = strictcli.App(name="test", version="1.0.0", help="test app", env_prefix="MYAPP")
184
+
185
+ @app.command("cmd", help="a command")
186
+ @strictcli.flag("rate", type=float, help="the rate", default=1.0, env="MYAPP_RATE")
187
+ def cmd(ctx, rate):
188
+ print(f"rate={rate}")
189
+
190
+ monkeypatch.setenv("MYAPP_RATE", "1e999")
191
+ r = app.test(["cmd"])
192
+ assert r.exit_code == 1
193
+ assert "expected float" in r.stderr
194
+ assert "(from env var 'MYAPP_RATE')" in r.stderr
195
+
196
+
161
197
  def test_float_flag_reject_whitespace():
162
198
  """Whitespace-padded values are rejected."""
163
199
  app = _make_app_with_float_flag()
@@ -88,6 +88,31 @@ def test_int_flag_bad_env_value(monkeypatch):
88
88
  assert "'abc'" in r.stderr
89
89
 
90
90
 
91
+ def test_int_flag_reject_underscore_separators():
92
+ """'1_000' (PEP 515 digit separators) is rejected to match Go/TypeScript."""
93
+ app = _make_app_with_int_flag()
94
+ r = app.test(["cmd", "--port", "1_000"])
95
+ assert r.exit_code == 1
96
+ assert "expected integer" in r.stderr
97
+ assert "'1_000'" in r.stderr
98
+
99
+
100
+ def test_int_flag_reject_underscore_env_value(monkeypatch):
101
+ """Underscore-separated int env var is rejected."""
102
+ app = strictcli.App(name="test", version="1.0.0", help="test app", env_prefix="MYAPP")
103
+
104
+ @app.command("cmd", help="a command")
105
+ @strictcli.flag("port", type=int, help="the port", default=80, env="MYAPP_PORT")
106
+ def cmd(ctx, port):
107
+ print(f"port={port}")
108
+
109
+ monkeypatch.setenv("MYAPP_PORT", "1_000")
110
+ r = app.test(["cmd"])
111
+ assert r.exit_code == 1
112
+ assert "expected integer" in r.stderr
113
+ assert "'1_000'" in r.stderr
114
+
115
+
91
116
  def test_int_flag_required():
92
117
  """Int flag with no default and omitted -> exit 1, stderr says required."""
93
118
  app = _make_app_with_int_flag()
@@ -126,6 +126,23 @@ def test_int_arg_rejects_float_string():
126
126
  assert "expected integer" in r.stderr
127
127
 
128
128
 
129
+ def test_int_arg_rejects_underscore_separators():
130
+ """Int arg rejects '1_000' (PEP 515 digit separators), matching Go/TypeScript."""
131
+ app = strictcli.App(name="test", version="1.0.0", help="test app")
132
+
133
+ @app.command(
134
+ "cmd",
135
+ help="a command",
136
+ args=[strictcli.Arg(name="count", help="a count", type=int)],
137
+ )
138
+ def cmd(ctx, count):
139
+ pass
140
+
141
+ r = app.test(["cmd", "1_000"])
142
+ assert r.exit_code == 1
143
+ assert "argument 'count': expected integer, got '1_000'" in r.stderr
144
+
145
+
129
146
  def test_int_arg_negative():
130
147
  """Int arg handles negative values (after -- separator)."""
131
148
  app = strictcli.App(name="test", version="1.0.0", help="test app")
@@ -211,6 +228,23 @@ def test_float_arg_rejects_inf():
211
228
  assert "Inf is not allowed" in r.stderr
212
229
 
213
230
 
231
+ def test_float_arg_rejects_overflow_to_infinity():
232
+ """Float arg rejects '1e999' (overflows to inf), using the generic parse error."""
233
+ app = strictcli.App(name="test", version="1.0.0", help="test app")
234
+
235
+ @app.command(
236
+ "cmd",
237
+ help="a command",
238
+ args=[strictcli.Arg(name="ratio", help="a ratio", type=float)],
239
+ )
240
+ def cmd(ctx, ratio):
241
+ pass
242
+
243
+ r = app.test(["cmd", "1e999"])
244
+ assert r.exit_code == 1
245
+ assert "argument 'ratio': expected float, got '1e999'" in r.stderr
246
+
247
+
214
248
  def test_float_arg_integer_string():
215
249
  """Float arg accepts integer string (coerces to float)."""
216
250
  app = strictcli.App(name="test", version="1.0.0", help="test app")
@@ -232,7 +232,7 @@ wheels = [
232
232
 
233
233
  [[package]]
234
234
  name = "strictcli"
235
- version = "0.32.1"
235
+ version = "0.32.2"
236
236
  source = { editable = "." }
237
237
  dependencies = [
238
238
  { name = "tomlkit" },
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