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.
- {strictcli-0.32.1 → strictcli-0.32.2}/PKG-INFO +1 -1
- {strictcli-0.32.1 → strictcli-0.32.2}/pyproject.toml +1 -1
- {strictcli-0.32.1 → strictcli-0.32.2}/strictcli/__init__.py +18 -9
- {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_coverage.py +3 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_float_type.py +36 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_int_type.py +25 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_typed_args.py +34 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/uv.lock +1 -1
- {strictcli-0.32.1 → strictcli-0.32.2}/.claude/settings.json +0 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/.github/workflows/ci.yml +0 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/.github/workflows/publish.yml +0 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/.gitignore +0 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/.rlsbl/bases/.github/workflows/ci.yml +0 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/.rlsbl/bases/.github/workflows/publish.yml +0 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/.rlsbl/bases/.gitignore +0 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/.rlsbl/bases/.rlsbl/lint/python.toml +0 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/.rlsbl/config.json +0 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/.rlsbl/lint/python.toml +0 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/.rlsbl/managed-files.json +0 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/.rlsbl/version +0 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/.strictcli/schema.json +0 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/CLAUDE.md +0 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/LICENSE +0 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/README.md +0 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/strictcli/py.typed +0 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/tests/conftest.py +0 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_arg_default.py +0 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_arg_default_validation.py +0 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_at_prefix.py +0 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_auto_version.py +0 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_call.py +0 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_check_command.py +0 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_check_discovery.py +0 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_check_provider.py +0 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_check_public_api.py +0 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_check_runner.py +0 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_check_schema.py +0 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_check_types.py +0 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_choices.py +0 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_choices_none.py +0 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_command_help_suggestion.py +0 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_command_tags.py +0 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_compound_types.py +0 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_config.py +0 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_config_fields.py +0 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_config_file_path.py +0 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_config_set_bugs.py +0 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_context.py +0 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_deep_nesting.py +0 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_dependencies.py +0 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_deprecated.py +0 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_dump_schema.py +0 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_e2e.py +0 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_env.py +0 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_exit_codes.py +0 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_flag_sets.py +0 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_float_format.py +0 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_float_vectors.py +0 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_global_flag_conflict_position.py +0 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_global_flags.py +0 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_help.py +0 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_hermetic.py +0 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_infra_env.py +0 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_invoke.py +0 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_keyword_flags.py +0 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_mcp.py +0 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_mutex.py +0 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_nesting.py +0 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_parser.py +0 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_passthrough.py +0 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_provenance.py +0 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_provenance_phase2.py +0 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_registration.py +0 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_repeatable.py +0 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_reserved_global_flags.py +0 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_tagdsl.py +0 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_toml_loading.py +0 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_tool_export.py +0 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_unique.py +0 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_utilities.py +0 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_validate.py +0 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_variadic.py +0 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/tests/test_visibility.py +0 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/todo/.defer/deferred.md +0 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/todo/.done/keyword-collision-in-flag-param-name.md +0 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/todo/.done/original-idea.md +0 -0
- {strictcli-0.32.1 → strictcli-0.32.2}/todo/.done/public-check-runner-api.md +0 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
__version__ = "0.32.
|
|
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
|
-
|
|
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()}
|
|
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
|
|
2851
|
-
shard file
|
|
2852
|
-
|
|
2853
|
-
|
|
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
|
|
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")
|
|
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
|