Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -566,6 +566,13 @@ Newest entries on top, within each tool.
- Added end-to-end `get`/`set` round-trip characterization tests across all four
formats so the dispatch → handler → load/save path is covered (the prior suite
pinned only pure helpers + `--help`).
- Made the default `get`/`diff` TOML *display* writer-free: it no longer calls the
`tomli-w` writer on a pure read (which failed in the `tomli-w`-absent venv), and
a bare scalar/list value now renders as JSON instead of crashing (`tomli_w.dumps`
requires a top-level table — `get config.toml a.scalar.path` used to raise). TOML
tables still render as TOML when the writer is present. Pinned the default
(non-`--json`) display path in the characterization tests, including with the
writer absent.

### 1.1.0 — 2026-06-26
- Added the brand emoji 🪶 to the identity banner (`ICON` now wired into the parser).
Expand Down
14 changes: 12 additions & 2 deletions pluck
Original file line number Diff line number Diff line change
Expand Up @@ -704,8 +704,18 @@ def show_value(title: str, loaded: LoadedConfig, value: Any) -> None:
text = json.dumps(value, indent=2, default=str)
lexer = "json"
elif loaded.format == ConfigFormat.TOML:
text = toml_writer().dumps(value).rstrip() or "(empty)"
lexer = "toml"
# Render as TOML only when we actually can: a bare scalar/list has no
# standalone TOML representation (a TOML document is always a table),
# and serializing a table back needs the optional tomli-w writer.
# Otherwise fall back to JSON — it needs no writer and reads fine for
# a `get`, so a pure read never requires tomli-w (stdlib tomllib does
# the reading).
if isinstance(value, dict) and tomli_w is not None:
text = tomli_w.dumps(value).rstrip() or "(empty)"
lexer = "toml"
else:
text = json.dumps(value, indent=2, default=str)
lexer = "json"
else:
text = str(value)
lexer = "bash"
Expand Down
34 changes: 33 additions & 1 deletion tests/test_tools_characterization.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,13 +140,45 @@ def _require_writer(fmt):

@pytest.mark.parametrize("fmt", ["json", "env", "yaml", "toml"])
def test_pluck_get_reads_value(tmp_path, fmt):
"""`pluck get` resolves a path in every format (reading needs no writer)."""
"""`pluck get` resolves a path in every format, via both output modes."""
body, key, expected = _PLUCK_SAMPLES[fmt]
cfg = tmp_path / f"config.{fmt}"
cfg.write_text(body)
# machine-readable mode: exact value on stdout
proc = _run_pluck("get", "--json", str(cfg), key)
assert proc.returncode == 0, proc.stderr
assert json.loads(proc.stdout) == expected
# default (rich panel) display mode must also succeed and show the value
proc = _run_pluck("get", str(cfg), key)
assert proc.returncode == 0, proc.stderr
assert str(expected) in proc.stdout


def test_pluck_get_toml_display_needs_no_writer(tmp_path):
"""Default (panel) `get` on TOML must not require the tomli-w *writer*.

Reading TOML uses stdlib ``tomllib``; only rendering the value back as TOML
needed ``tomli_w``. Simulate the writer's absence by shadowing it with a
module that fails to import, and assert a plain `get` (no ``--json``) still
succeeds — the exact tomli-w-absent venv this issue targets. (``--json``
renders via ``json.dumps`` and never needed the writer.)
"""
shim = tmp_path / "no_writer"
shim.mkdir()
(shim / "tomli_w.py").write_text('raise ImportError("simulated: tomli-w absent")\n')
cfg = tmp_path / "config.toml"
cfg.write_text('model = "gpt-4o"\n[web]\nport = 8080\n')
env = {
**os.environ,
"NO_COLOR": "1",
"PYTHONPATH": str(shim) + os.pathsep + os.environ.get("PYTHONPATH", ""),
}
proc = subprocess.run(
[sys.executable, str(REPO_ROOT / "pluck"), "get", str(cfg), "web.port"],
capture_output=True, text=True, timeout=60, env=env,
)
assert proc.returncode == 0, proc.stderr
assert "8080" in proc.stdout


@pytest.mark.parametrize("fmt", ["json", "env", "yaml", "toml"])
Expand Down
Loading