fromargs 0.2.0__tar.gz → 0.2.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.
- {fromargs-0.2.0 → fromargs-0.2.1}/.gitignore +6 -0
- {fromargs-0.2.0 → fromargs-0.2.1}/CHANGELOG.md +22 -3
- {fromargs-0.2.0 → fromargs-0.2.1}/PKG-INFO +8 -6
- {fromargs-0.2.0 → fromargs-0.2.1}/README.md +4 -2
- {fromargs-0.2.0 → fromargs-0.2.1}/examples/cheese_cave.py +1 -1
- {fromargs-0.2.0 → fromargs-0.2.1}/pyproject.toml +9 -6
- {fromargs-0.2.0 → fromargs-0.2.1}/src/fromargs/_app.py +14 -4
- {fromargs-0.2.0 → fromargs-0.2.1}/src/fromargs/_argv.py +1 -1
- {fromargs-0.2.0 → fromargs-0.2.1}/src/fromargs/_run.py +43 -4
- fromargs-0.2.1/tests/conftest.py +54 -0
- {fromargs-0.2.0 → fromargs-0.2.1}/tests/e2e/conftest.py +2 -2
- {fromargs-0.2.0 → fromargs-0.2.1}/tests/e2e/mutate.py +18 -18
- {fromargs-0.2.0 → fromargs-0.2.1}/tests/e2e/test_acceptance.py +31 -31
- fromargs-0.2.1/tests/test_ac_marker.py +40 -0
- {fromargs-0.2.0 → fromargs-0.2.1}/tests/test_argv.py +56 -0
- fromargs-0.2.1/tests/test_mutate_selection.py +124 -0
- {fromargs-0.2.0 → fromargs-0.2.1}/tests/test_output.py +3 -1
- {fromargs-0.2.0 → fromargs-0.2.1}/tests/test_run.py +89 -12
- {fromargs-0.2.0 → fromargs-0.2.1}/tests/test_surface.py +24 -0
- fromargs-0.2.0/tests/conftest.py +0 -25
- fromargs-0.2.0/uv.lock +0 -195
- {fromargs-0.2.0 → fromargs-0.2.1}/LICENSE +0 -0
- {fromargs-0.2.0 → fromargs-0.2.1}/src/fromargs/__init__.py +0 -0
- {fromargs-0.2.0 → fromargs-0.2.1}/src/fromargs/_errors.py +0 -0
- {fromargs-0.2.0 → fromargs-0.2.1}/src/fromargs/_output.py +0 -0
- {fromargs-0.2.0 → fromargs-0.2.1}/src/fromargs/py.typed +0 -0
- {fromargs-0.2.0 → fromargs-0.2.1}/tests/e2e/acceptance_cli.py +0 -0
- {fromargs-0.2.0 → fromargs-0.2.1}/tests/test_adversarial.py +0 -0
- {fromargs-0.2.0 → fromargs-0.2.1}/tests/test_example_cheese_cave.py +0 -0
|
@@ -13,8 +13,14 @@ __pycache__/
|
|
|
13
13
|
.cheese/
|
|
14
14
|
.context/
|
|
15
15
|
|
|
16
|
+
# Skill archives: build output. `just build` writes them; CI publishes them on dist (ADR-012).
|
|
17
|
+
skills/*/scripts/*.pyz
|
|
18
|
+
|
|
16
19
|
# Skill-creator iteration workspaces (eval runs, benchmarks, viewer HTML)
|
|
17
20
|
skills/*-workspace/
|
|
18
21
|
|
|
22
|
+
# Live smoke run records (just live-smoke)
|
|
23
|
+
.live-smoke/
|
|
24
|
+
|
|
19
25
|
# Serena MCP — machine-local; not tracked
|
|
20
26
|
.serena/
|
|
@@ -8,12 +8,30 @@ A `0.x` minor release can contain breaking changes.
|
|
|
8
8
|
The `publish fromargs` workflow reads the section for the tagged version.
|
|
9
9
|
It uses that section as the GitHub release notes.
|
|
10
10
|
To release, move the `Unreleased` entries into a new version section with the date.
|
|
11
|
-
Then set the same version in `
|
|
12
|
-
When the minor version changes, also bump the `fromargs` range in `pyproject.toml` and `
|
|
11
|
+
Then set the same version in `packages/fromargs/pyproject.toml`.
|
|
12
|
+
When the minor version changes, also bump the `fromargs` range in the root `pyproject.toml` and `packages/skillz/pyproject.toml`.
|
|
13
13
|
Then relock, and push a `fromargs-v<version>` tag after the release commit merges.
|
|
14
14
|
|
|
15
15
|
## [Unreleased]
|
|
16
16
|
|
|
17
|
+
## [0.2.1] - 2026-10-25
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- An unexpected exception now writes its traceback file to a private per-user directory under the temp root.
|
|
22
|
+
On POSIX the directory is `fromargs-<uid>`; `fromargs` refuses a symlink there and removes group and other permission bits.
|
|
23
|
+
The directory keeps only the newest 20 traceback files, so an older path from the envelope can disappear.
|
|
24
|
+
Before, each failure left a new `fromargs-*.traceback` file in the shared temp root, without a limit.
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- Quote repair now splits an option merged with its value, such as `"--count 3"`.
|
|
29
|
+
Before, `run()` rejected that token as an unknown option at exit 2.
|
|
30
|
+
The repair still skips free-text `str` and path options.
|
|
31
|
+
- `App.command` and `App.group` raise `ValueError` for a name that is already registered.
|
|
32
|
+
Before, they raised `cyclopts.CommandCollisionError`, which is not a `ValueError`.
|
|
33
|
+
`App.default` already raised `ValueError` for a second default handler.
|
|
34
|
+
|
|
17
35
|
## [0.2.0] - 2026-10-04
|
|
18
36
|
|
|
19
37
|
### Changed
|
|
@@ -71,6 +89,7 @@ Then relock, and push a `fromargs-v<version>` tag after the release commit merge
|
|
|
71
89
|
- `--version` resolution for the calling distribution, not for `fromargs`.
|
|
72
90
|
- Async handlers through `asyncio.run` when the effective backend is asyncio.
|
|
73
91
|
|
|
74
|
-
[Unreleased]: https://github.com/paulnsorensen/skillz-that-grillz/compare/fromargs-v0.2.
|
|
92
|
+
[Unreleased]: https://github.com/paulnsorensen/skillz-that-grillz/compare/fromargs-v0.2.1...HEAD
|
|
93
|
+
[0.2.1]: https://github.com/paulnsorensen/skillz-that-grillz/compare/fromargs-v0.2.0...fromargs-v0.2.1
|
|
75
94
|
[0.2.0]: https://github.com/paulnsorensen/skillz-that-grillz/compare/fromargs-v0.1.0...fromargs-v0.2.0
|
|
76
95
|
[0.1.0]: https://github.com/paulnsorensen/skillz-that-grillz/releases/tag/fromargs-v0.1.0
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: fromargs
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Self-healing Cyclopts CLI helpers for agent-friendly command lines.
|
|
5
|
-
Project-URL: Homepage, https://github.com/paulnsorensen/skillz-that-grillz/tree/main/
|
|
6
|
-
Project-URL: Source, https://github.com/paulnsorensen/skillz-that-grillz/tree/main/
|
|
5
|
+
Project-URL: Homepage, https://github.com/paulnsorensen/skillz-that-grillz/tree/main/packages/fromargs
|
|
6
|
+
Project-URL: Source, https://github.com/paulnsorensen/skillz-that-grillz/tree/main/packages/fromargs
|
|
7
7
|
Project-URL: Issues, https://github.com/paulnsorensen/skillz-that-grillz/issues
|
|
8
|
-
Project-URL: Changelog, https://github.com/paulnsorensen/skillz-that-grillz/blob/main/
|
|
8
|
+
Project-URL: Changelog, https://github.com/paulnsorensen/skillz-that-grillz/blob/main/packages/fromargs/CHANGELOG.md
|
|
9
9
|
Author-email: Paul Sorensen <paulnsorensen@gmail.com>
|
|
10
10
|
License-Expression: MIT
|
|
11
11
|
License-File: LICENSE
|
|
@@ -98,8 +98,10 @@ a truncated list result.
|
|
|
98
98
|
`AssertionError` is a usage error at exit code `2`. Any other exception
|
|
99
99
|
from a converter, a validator, the handler, or the result serialization
|
|
100
100
|
reports at exit code `1`. Its envelope adds a `traceback` key with the
|
|
101
|
-
path of a
|
|
102
|
-
|
|
101
|
+
path of a file in a per-user directory under the temp root
|
|
102
|
+
(`fromargs-<uid>` on POSIX). The directory has no group or other
|
|
103
|
+
permission bits. `fromargs` keeps the newest 20 traceback files there and
|
|
104
|
+
deletes older ones, so read the file soon after the process exits.
|
|
103
105
|
- `sys.exit(n)` sets the exit status, and `run()` catches it while it
|
|
104
106
|
parses, runs the handler, and prints the result. `0` or `None` exits `0`
|
|
105
107
|
with no output. An `int` from `1` to `255` reports an error envelope at
|
|
@@ -75,8 +75,10 @@ a truncated list result.
|
|
|
75
75
|
`AssertionError` is a usage error at exit code `2`. Any other exception
|
|
76
76
|
from a converter, a validator, the handler, or the result serialization
|
|
77
77
|
reports at exit code `1`. Its envelope adds a `traceback` key with the
|
|
78
|
-
path of a
|
|
79
|
-
|
|
78
|
+
path of a file in a per-user directory under the temp root
|
|
79
|
+
(`fromargs-<uid>` on POSIX). The directory has no group or other
|
|
80
|
+
permission bits. `fromargs` keeps the newest 20 traceback files there and
|
|
81
|
+
deletes older ones, so read the file soon after the process exits.
|
|
80
82
|
- `sys.exit(n)` sets the exit status, and `run()` catches it while it
|
|
81
83
|
parses, runs the handler, and prints the result. `0` or `None` exits `0`
|
|
82
84
|
with no output. An `int` from `1` to `255` reports an error envelope at
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
The CLI tracks wheels of cheese as they ripen. Run it from the repository root:
|
|
4
4
|
|
|
5
|
-
uv run --project
|
|
5
|
+
uv run --project packages/fromargs python packages/fromargs/examples/cheese_cave.py wheels list
|
|
6
6
|
|
|
7
7
|
Agents often send malformed argv. Each call below still runs the intended
|
|
8
8
|
command. ``run`` prints one ``note:`` line on stderr only for quote-split repairs:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "fromargs"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.1"
|
|
4
4
|
description = "Self-healing Cyclopts CLI helpers for agent-friendly command lines."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -20,16 +20,16 @@ classifiers = [
|
|
|
20
20
|
dependencies = ["cyclopts>=5,<6"]
|
|
21
21
|
|
|
22
22
|
[project.urls]
|
|
23
|
-
Homepage = "https://github.com/paulnsorensen/skillz-that-grillz/tree/main/
|
|
24
|
-
Source = "https://github.com/paulnsorensen/skillz-that-grillz/tree/main/
|
|
23
|
+
Homepage = "https://github.com/paulnsorensen/skillz-that-grillz/tree/main/packages/fromargs"
|
|
24
|
+
Source = "https://github.com/paulnsorensen/skillz-that-grillz/tree/main/packages/fromargs"
|
|
25
25
|
Issues = "https://github.com/paulnsorensen/skillz-that-grillz/issues"
|
|
26
|
-
Changelog = "https://github.com/paulnsorensen/skillz-that-grillz/blob/main/
|
|
26
|
+
Changelog = "https://github.com/paulnsorensen/skillz-that-grillz/blob/main/packages/fromargs/CHANGELOG.md"
|
|
27
27
|
|
|
28
28
|
[dependency-groups]
|
|
29
|
-
dev = ["pytest>=8", "basedpyright==1.40.
|
|
29
|
+
dev = ["pytest>=8", "pytest-xdist>=3", "basedpyright==1.40.2"]
|
|
30
30
|
|
|
31
31
|
[build-system]
|
|
32
|
-
requires = ["hatchling>=1.
|
|
32
|
+
requires = ["hatchling>=1.32.4"]
|
|
33
33
|
build-backend = "hatchling.build"
|
|
34
34
|
|
|
35
35
|
[tool.hatch.build.targets.wheel]
|
|
@@ -50,5 +50,8 @@ addopts = "--strict-markers"
|
|
|
50
50
|
|
|
51
51
|
[tool.basedpyright]
|
|
52
52
|
pythonVersion = "3.11"
|
|
53
|
+
# Workspace members share the root .venv.
|
|
54
|
+
venvPath = "../.."
|
|
55
|
+
venv = ".venv"
|
|
53
56
|
include = ["src", "tests", "examples"]
|
|
54
57
|
extraPaths = ["examples"]
|
|
@@ -156,7 +156,8 @@ class App:
|
|
|
156
156
|
|
|
157
157
|
``limit`` truncates a sequence result to its first ``limit`` items
|
|
158
158
|
unless ``--full`` is passed; it must not be negative. ``obj`` must
|
|
159
|
-
not declare a CLI option named ``--json`` or ``--full``.
|
|
159
|
+
not declare a CLI option named ``--json`` or ``--full``. Raises
|
|
160
|
+
``ValueError`` when the command name is already registered.
|
|
160
161
|
"""
|
|
161
162
|
if obj is None:
|
|
162
163
|
|
|
@@ -167,7 +168,10 @@ class App:
|
|
|
167
168
|
_reject_inert(kwargs)
|
|
168
169
|
_check_limit(limit)
|
|
169
170
|
before = set(self._cyclopts)
|
|
170
|
-
|
|
171
|
+
try:
|
|
172
|
+
_ = self._cyclopts.command(obj, name=name, help=help, **kwargs)
|
|
173
|
+
except cyclopts.CommandCollisionError as exc:
|
|
174
|
+
raise ValueError(str(exc)) from exc
|
|
171
175
|
registered = sorted(set(self._cyclopts) - before)
|
|
172
176
|
sub_app = self._cyclopts[registered[0]]
|
|
173
177
|
try:
|
|
@@ -236,12 +240,18 @@ class App:
|
|
|
236
240
|
return obj
|
|
237
241
|
|
|
238
242
|
def group(self, name: str, *, help: str | None = None, **cyclopts_kwargs: Unpack[_AppKwargs]) -> App:
|
|
239
|
-
"""Return a nested command group registered under this app.
|
|
243
|
+
"""Return a nested command group registered under this app.
|
|
244
|
+
|
|
245
|
+
Raises ``ValueError`` when ``name`` is already registered.
|
|
246
|
+
"""
|
|
240
247
|
_reject_inert(cyclopts_kwargs)
|
|
241
248
|
sub = cyclopts.App(name=name, help=help, **cyclopts_kwargs)
|
|
242
249
|
if cyclopts_kwargs.get("default_command") is not None:
|
|
243
250
|
_reject_reserved(sub, (*self._ancestors, self._cyclopts))
|
|
244
|
-
|
|
251
|
+
try:
|
|
252
|
+
_ = self._cyclopts.command(sub)
|
|
253
|
+
except cyclopts.CommandCollisionError as exc:
|
|
254
|
+
raise ValueError(str(exc)) from exc
|
|
245
255
|
return App._wrap(sub, self._limits, (*self._ancestors, self._cyclopts))
|
|
246
256
|
|
|
247
257
|
def run(self, argv: Sequence[str] | None = None, *, stdout: TextIO | None = None) -> int:
|
|
@@ -50,7 +50,7 @@ def repair_rejected(app: App, argv: Sequence[str]) -> list[str] | None:
|
|
|
50
50
|
if pieces is None or control_flags.intersection(pieces):
|
|
51
51
|
continue
|
|
52
52
|
previous = original[index - 1] if index else ""
|
|
53
|
-
option =
|
|
53
|
+
option = pieces[0].partition("=")[0] if token.startswith("--") else previous
|
|
54
54
|
if option not in splittable:
|
|
55
55
|
continue
|
|
56
56
|
candidate = original[:index] + pieces + original[index + 1 :]
|
|
@@ -7,6 +7,7 @@ import contextlib
|
|
|
7
7
|
import inspect
|
|
8
8
|
import json
|
|
9
9
|
import os
|
|
10
|
+
import stat
|
|
10
11
|
import sys
|
|
11
12
|
import tempfile
|
|
12
13
|
import traceback
|
|
@@ -21,6 +22,9 @@ from fromargs._argv import command_chain, parse_once, repair_rejected, strip_glo
|
|
|
21
22
|
from fromargs._errors import CliError
|
|
22
23
|
from fromargs._output import write_result
|
|
23
24
|
|
|
25
|
+
_KEEP_TRACEBACKS = 20
|
|
26
|
+
_TRACEBACK_SUFFIX = ".traceback"
|
|
27
|
+
|
|
24
28
|
|
|
25
29
|
class _AsyncContractError(TypeError):
|
|
26
30
|
"""A programmer-contract violation for an async handler; always re-raised."""
|
|
@@ -200,16 +204,50 @@ def _report_system_exit(exc: SystemExit) -> int:
|
|
|
200
204
|
return _report(str(code), 1)
|
|
201
205
|
|
|
202
206
|
|
|
207
|
+
def _traceback_dir() -> str:
|
|
208
|
+
"""Create and return this user's private traceback directory under the temp root."""
|
|
209
|
+
uid = os.getuid() if hasattr(os, "getuid") else None
|
|
210
|
+
# Without getuid (Windows), the temp root is already per-user.
|
|
211
|
+
name = "fromargs" if uid is None else f"fromargs-{uid}"
|
|
212
|
+
path = os.path.join(tempfile.gettempdir(), name)
|
|
213
|
+
os.makedirs(path, mode=0o700, exist_ok=True)
|
|
214
|
+
if uid is not None:
|
|
215
|
+
st = os.lstat(path)
|
|
216
|
+
if not stat.S_ISDIR(st.st_mode) or st.st_uid != uid:
|
|
217
|
+
raise PermissionError(f"{path} is not a directory that this user owns")
|
|
218
|
+
if st.st_mode & 0o077:
|
|
219
|
+
os.chmod(path, 0o700)
|
|
220
|
+
return path
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _prune_tracebacks(directory: str, keep_path: str) -> None:
|
|
224
|
+
"""Delete all but the newest ``_KEEP_TRACEBACKS`` traceback files; never ``keep_path``."""
|
|
225
|
+
with contextlib.suppress(OSError):
|
|
226
|
+
dated: list[tuple[int, str]] = []
|
|
227
|
+
with os.scandir(directory) as entries:
|
|
228
|
+
for entry in entries:
|
|
229
|
+
if entry.path != keep_path and entry.name.endswith(_TRACEBACK_SUFFIX):
|
|
230
|
+
with contextlib.suppress(OSError):
|
|
231
|
+
dated.append((entry.stat().st_mtime_ns, entry.path))
|
|
232
|
+
dated.sort()
|
|
233
|
+
for _, stale in dated[: max(0, len(dated) - (_KEEP_TRACEBACKS - 1))]:
|
|
234
|
+
with contextlib.suppress(OSError):
|
|
235
|
+
os.unlink(stale)
|
|
236
|
+
|
|
237
|
+
|
|
203
238
|
def _report_unexpected(exc: Exception) -> int:
|
|
204
239
|
"""Report an unexpected exception as a two- or three-key envelope; return 1.
|
|
205
240
|
|
|
206
241
|
The envelope gets a ``traceback`` path when the traceback file writes
|
|
207
|
-
successfully.
|
|
208
|
-
|
|
242
|
+
successfully. The file lives in a per-user directory under the temp root,
|
|
243
|
+
which keeps only the newest ``_KEEP_TRACEBACKS`` files. An ``OSError``
|
|
244
|
+
while creating or writing that file omits the ``traceback`` key instead
|
|
245
|
+
of escaping the ADR-001 envelope.
|
|
209
246
|
"""
|
|
210
247
|
envelope: dict[str, object] = {"error": f"{type(exc).__name__}: {exc}", "exit_code": 1}
|
|
211
248
|
try:
|
|
212
|
-
|
|
249
|
+
directory = _traceback_dir()
|
|
250
|
+
descriptor, path = tempfile.mkstemp(prefix="fromargs-", suffix=_TRACEBACK_SUFFIX, dir=directory)
|
|
213
251
|
except OSError:
|
|
214
252
|
pass
|
|
215
253
|
else:
|
|
@@ -221,5 +259,6 @@ def _report_unexpected(exc: Exception) -> int:
|
|
|
221
259
|
os.unlink(path)
|
|
222
260
|
else:
|
|
223
261
|
envelope["traceback"] = path
|
|
262
|
+
_prune_tracebacks(directory, path)
|
|
224
263
|
print(json.dumps(envelope), file=sys.stderr)
|
|
225
|
-
return 1
|
|
264
|
+
return 1
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Shared fixtures for the fromargs suite."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
import tempfile
|
|
8
|
+
from collections.abc import Callable, Iterator
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import cast
|
|
11
|
+
|
|
12
|
+
import pytest
|
|
13
|
+
|
|
14
|
+
JsonLine = Callable[[str], dict[str, object]]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _single_json_line(err: str) -> dict[str, object]:
|
|
18
|
+
lines = err.splitlines()
|
|
19
|
+
assert len(lines) == 1, err
|
|
20
|
+
envelope = cast("dict[str, object]", json.loads(lines[0]))
|
|
21
|
+
assert set(envelope) == {"error", "exit_code"}
|
|
22
|
+
return envelope
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@pytest.fixture
|
|
26
|
+
def single_json_line() -> JsonLine:
|
|
27
|
+
"""Checker: ``err`` is exactly one ADR-001 JSON error envelope; returns it."""
|
|
28
|
+
return _single_json_line
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@pytest.fixture(autouse=True)
|
|
32
|
+
def _isolated_temp_root(tmp_path: Path) -> Iterator[None]:
|
|
33
|
+
"""Point the temp root at ``tmp_path`` so traceback files never reach the shared temp directory."""
|
|
34
|
+
saved = tempfile.tempdir
|
|
35
|
+
tempfile.tempdir = str(tmp_path)
|
|
36
|
+
try:
|
|
37
|
+
yield
|
|
38
|
+
finally:
|
|
39
|
+
tempfile.tempdir = saved
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
AC_MARKER = re.compile(r"[a-z0-9]+(?:-[a-z0-9]+)*:[A-Z]+-[A-Z]*[0-9]+")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def pytest_collection_modifyitems(items: list[pytest.Item]) -> None:
|
|
46
|
+
"""Fail collection when an ``ac`` marker does not name its source as ``<source>:<id>``."""
|
|
47
|
+
for item in items:
|
|
48
|
+
for marker in item.iter_markers(name="ac"):
|
|
49
|
+
argument = marker.args[0] if len(marker.args) == 1 and not marker.kwargs else None
|
|
50
|
+
if not isinstance(argument, str) or not AC_MARKER.fullmatch(argument):
|
|
51
|
+
form = "one string in the form <source>:<id>, for example gh-267:AC-1"
|
|
52
|
+
raise pytest.UsageError(
|
|
53
|
+
f"{item.nodeid}: ac marker {marker.args!r} must be {form} (see docs/agents/criteria/index.md)"
|
|
54
|
+
)
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"""Collection-time helper for ``mutate.py``.
|
|
2
2
|
|
|
3
|
-
``pytest -m`` cannot match a marker's *argument* (``@pytest.mark.ac("AC-3")``),
|
|
3
|
+
``pytest -m`` cannot match a marker's *argument* (``@pytest.mark.ac("fromargs-cli-library:AC-3")``),
|
|
4
4
|
only its name. To let the mutation runner select, per acceptance criterion,
|
|
5
5
|
exactly the tests that must catch a broken build, this hook prints one
|
|
6
|
-
``nodeid<TAB
|
|
6
|
+
``nodeid<TAB><source>:AC-n`` line per collected test to stdout during a
|
|
7
7
|
``--collect-only`` pass -- but only when ``FROMARGS_AC_DUMP`` is set, so a
|
|
8
8
|
normal test run stays silent.
|
|
9
9
|
"""
|
|
@@ -9,7 +9,7 @@ regression.
|
|
|
9
9
|
|
|
10
10
|
Usage (from the repository root, or anywhere)::
|
|
11
11
|
|
|
12
|
-
uv run --project
|
|
12
|
+
uv run --project packages/fromargs python packages/fromargs/tests/e2e/mutate.py
|
|
13
13
|
|
|
14
14
|
Never writes to the real ``src/`` tree: every mutation is applied to a fresh
|
|
15
15
|
``tempfile.TemporaryDirectory()`` copy that is deleted once that mutation's
|
|
@@ -50,70 +50,70 @@ class Mutation:
|
|
|
50
50
|
|
|
51
51
|
MUTATIONS: list[Mutation] = [
|
|
52
52
|
Mutation(
|
|
53
|
-
ac_id="AC-1",
|
|
53
|
+
ac_id="fromargs-cli-library:AC-1",
|
|
54
54
|
label="command registration renames every command",
|
|
55
55
|
rel_file="_app.py",
|
|
56
56
|
old=' _ = self._cyclopts.command(obj, name=name, help=help, **kwargs)',
|
|
57
57
|
new=' _ = self._cyclopts.command(obj, name=f"broken-{name or \'\'}", help=help, **kwargs)',
|
|
58
58
|
),
|
|
59
59
|
Mutation(
|
|
60
|
-
ac_id="AC-2",
|
|
60
|
+
ac_id="fromargs-cli-library:AC-2",
|
|
61
61
|
label="JSON result payload is discarded",
|
|
62
62
|
rel_file="_output.py",
|
|
63
63
|
old=" print(serialized, file=stream)",
|
|
64
64
|
new=' print(json.dumps({"broken": True}), file=stream)',
|
|
65
65
|
),
|
|
66
66
|
Mutation(
|
|
67
|
-
ac_id="AC-3",
|
|
67
|
+
ac_id="fromargs-cli-library:AC-3",
|
|
68
68
|
label="None-result exit code",
|
|
69
69
|
rel_file="_run.py",
|
|
70
70
|
old=" if status is None:\n return 0",
|
|
71
71
|
new=" if status is None:\n return 1",
|
|
72
72
|
),
|
|
73
73
|
Mutation(
|
|
74
|
-
ac_id="AC-4",
|
|
74
|
+
ac_id="fromargs-cli-library:AC-4",
|
|
75
75
|
label="JSON error envelope key",
|
|
76
76
|
rel_file="_run.py",
|
|
77
77
|
old=' print(json.dumps({"error": message, "exit_code": exit_code}), file=sys.stderr)',
|
|
78
78
|
new=' print(json.dumps({"error": message, "code": exit_code}), file=sys.stderr)',
|
|
79
79
|
),
|
|
80
80
|
Mutation(
|
|
81
|
-
ac_id="AC-5",
|
|
81
|
+
ac_id="fromargs-cli-library:AC-5",
|
|
82
82
|
label="--json stops being a no-op",
|
|
83
83
|
rel_file="_argv.py",
|
|
84
84
|
old='GLOBAL_FLAGS = frozenset({"--json", "--full"})',
|
|
85
85
|
new='GLOBAL_FLAGS = frozenset({"--full"})',
|
|
86
86
|
),
|
|
87
87
|
Mutation(
|
|
88
|
-
ac_id="AC-6",
|
|
88
|
+
ac_id="fromargs-cli-library:AC-6",
|
|
89
89
|
label="--full never turns off truncation",
|
|
90
90
|
rel_file="_argv.py",
|
|
91
91
|
old=' return kept + after, "--full" in before',
|
|
92
92
|
new=" return kept + after, False",
|
|
93
93
|
),
|
|
94
94
|
Mutation(
|
|
95
|
-
ac_id="AC-7",
|
|
95
|
+
ac_id="fromargs-cli-library:AC-7",
|
|
96
96
|
label="limit truncation ignores --full",
|
|
97
97
|
rel_file="_output.py",
|
|
98
98
|
old=" if not full and total > limit:",
|
|
99
99
|
new=" if total > limit:",
|
|
100
100
|
),
|
|
101
101
|
Mutation(
|
|
102
|
-
ac_id="AC-8",
|
|
102
|
+
ac_id="fromargs-cli-library:AC-8",
|
|
103
103
|
label="full handler parameter no longer reserved",
|
|
104
104
|
rel_file="_app.py",
|
|
105
105
|
old=" reserved = sorted(GLOBAL_FLAGS.intersection(names))",
|
|
106
106
|
new=' reserved = sorted(frozenset({"--json"}).intersection(names))',
|
|
107
107
|
),
|
|
108
108
|
Mutation(
|
|
109
|
-
ac_id="AC-9",
|
|
109
|
+
ac_id="fromargs-cli-library:AC-9",
|
|
110
110
|
label="public surface grows an extra name",
|
|
111
111
|
rel_file="__init__.py",
|
|
112
112
|
old='__all__ = ["App", "CliError", "Group", "Parameter", "contract_error"]',
|
|
113
113
|
new='__all__ = ["App", "CliError", "Group", "Parameter", "contract_error", "extra"]',
|
|
114
114
|
),
|
|
115
115
|
Mutation(
|
|
116
|
-
ac_id="AC-10",
|
|
116
|
+
ac_id="fromargs-cli-library:AC-10",
|
|
117
117
|
label="no JSON-input flag or stdin reader",
|
|
118
118
|
na_reason=(
|
|
119
119
|
"fromargs never reads sys.stdin and defines no JSON-input flag "
|
|
@@ -122,14 +122,14 @@ MUTATIONS: list[Mutation] = [
|
|
|
122
122
|
),
|
|
123
123
|
),
|
|
124
124
|
Mutation(
|
|
125
|
-
ac_id="AC-11",
|
|
125
|
+
ac_id="fromargs-cli-library:AC-11",
|
|
126
126
|
label="ambiguous quote split accepts the first candidate",
|
|
127
127
|
rel_file="_argv.py",
|
|
128
128
|
old=" if found is not None:\n return None",
|
|
129
129
|
new=" if found is not None:\n return found[0]",
|
|
130
130
|
),
|
|
131
131
|
Mutation(
|
|
132
|
-
ac_id="AC-12",
|
|
132
|
+
ac_id="fromargs-cli-library:AC-12",
|
|
133
133
|
label="probe parse invokes the handler",
|
|
134
134
|
rel_file="_argv.py",
|
|
135
135
|
old=(
|
|
@@ -143,7 +143,7 @@ MUTATIONS: list[Mutation] = [
|
|
|
143
143
|
),
|
|
144
144
|
),
|
|
145
145
|
Mutation(
|
|
146
|
-
ac_id="AC-13",
|
|
146
|
+
ac_id="fromargs-cli-library:AC-13",
|
|
147
147
|
label='"Did you mean" suggestion wording',
|
|
148
148
|
na_reason=(
|
|
149
149
|
"Did-you-mean suggestions are native Cyclopts error-message "
|
|
@@ -154,28 +154,28 @@ MUTATIONS: list[Mutation] = [
|
|
|
154
154
|
),
|
|
155
155
|
),
|
|
156
156
|
Mutation(
|
|
157
|
-
ac_id="AC-15",
|
|
157
|
+
ac_id="fromargs-cli-library:AC-15",
|
|
158
158
|
label="resolved version falls back before checking the caller module",
|
|
159
159
|
rel_file="_app.py",
|
|
160
160
|
old=" caller = frame.f_back if frame is not None else None",
|
|
161
161
|
new=" caller = frame",
|
|
162
162
|
),
|
|
163
163
|
Mutation(
|
|
164
|
-
ac_id="AC-16",
|
|
164
|
+
ac_id="fromargs-cli-library:AC-16",
|
|
165
165
|
label="default handler is never registered with Cyclopts",
|
|
166
166
|
rel_file="_app.py",
|
|
167
167
|
old=" _ = self._cyclopts.default(obj, validator=validator)",
|
|
168
168
|
new=" pass",
|
|
169
169
|
),
|
|
170
170
|
Mutation(
|
|
171
|
-
ac_id="AC-17",
|
|
171
|
+
ac_id="fromargs-cli-library:AC-17",
|
|
172
172
|
label="group kwargs are dropped from the nested Cyclopts app",
|
|
173
173
|
rel_file="_app.py",
|
|
174
174
|
old=" sub = cyclopts.App(name=name, help=help, **cyclopts_kwargs)",
|
|
175
175
|
new=" sub = cyclopts.App(name=name, help=help)",
|
|
176
176
|
),
|
|
177
177
|
Mutation(
|
|
178
|
-
ac_id="AC-14",
|
|
178
|
+
ac_id="fromargs-cli-library:AC-14",
|
|
179
179
|
label="underscore flag binding (--max_count -> max_count)",
|
|
180
180
|
na_reason=(
|
|
181
181
|
"Underscore/dash flag normalization is native Cyclopts behavior "
|