From a95fedeb0e15c2fb67fd4131b7cb71a6d93d656c Mon Sep 17 00:00:00 2001 From: Alpamys Date: Fri, 17 Jul 2026 20:40:54 +0500 Subject: [PATCH] fix(cli): quote install hints so `pip install soup-cli[extra]` works on cmd.exe (v0.71.37) Every printed and documented `pip install 'soup-cli[extra]'` was bash / zsh / PowerShell syntax and failed on Windows cmd.exe: ERROR: Invalid requirement: "'soup-cli[train]'": Expected package name at the start of dependency specifier cmd.exe has no single-quote quoting, so it passes the quotes to pip verbatim and pip rejects the requirement. Nothing in Soup can fix that once the command is typed -- pip and the shell own it, and Soup is not installed yet when the README line runs -- so the fix is the spelling we print. Migrated 147 sites across 67 files to `pip install "soup-cli[extra]"`: - 64 in src/ (Rich console hints + plain ImportError text) - 57 in README.md + docs/ - 22 in src/soup_cli/templates/*.yaml + examples/configs/*.yaml - 3 in examples/README.md Double quotes are the only spelling valid in every shell (cmd, PowerShell, bash, zsh), which is why the repo already used `pip install -e ".[dev]"`. Measured on Windows: single quotes fail ONLY on cmd; double quotes pass everywhere; bare passes on Windows but zsh globs `[extra]` and fails. Method note (the PR #247 class): the hints sit INSIDE double-quoted Python string literals, so a blind ' -> " sed produces SyntaxError. A tokenize-based rewriter escaped `\"` in DQUOTE tokens and left bare `"` in TRIPLE / COMMENT tokens; every touched .py was compile-checked. The full suite (not ruff, not compile-check) caught two rewriter blind spots: the real YAML templates under src/soup_cli/templates/ (byte-identical drift test) and examples/README.md. A regression test (tests/test_v07137.py) scans the package and every docs code block for the single-quoted form; prose may still name it so a reader from an older tutorial recognises the error. Also bundles #315 (@Sanjays2402): eval-gate benchmark tasks now run via ForgettingDetector instead of a helper that never existed. Closes #310. Test count: 16283 -> 16288 (+4 in tests/test_v07137.py). Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 27 ++++ CONTRIBUTING.md | 2 +- README.md | 41 ++++-- docs/backends-and-ops.md | 8 +- docs/commands.md | 6 +- docs/data.md | 6 +- docs/evaluation.md | 2 +- docs/models.md | 46 +++--- docs/performance-and-quantization.md | 10 +- docs/serving-and-export.md | 12 +- docs/training.md | 4 +- examples/README.md | 6 +- examples/configs/dpo_example.yaml | 4 +- pyproject.toml | 2 +- src/soup_cli/__init__.py | 2 +- src/soup_cli/cloud/modal.py | 2 +- src/soup_cli/commands/compile_cmd.py | 2 +- src/soup_cli/commands/data.py | 8 +- src/soup_cli/commands/data_canary.py | 2 +- src/soup_cli/commands/data_topics.py | 2 +- src/soup_cli/commands/eval.py | 2 +- src/soup_cli/commands/export.py | 8 +- src/soup_cli/commands/generate.py | 2 +- src/soup_cli/commands/mcp.py | 2 +- .../commands/merge_sharded_fsdp_weights.py | 2 +- src/soup_cli/commands/serve.py | 6 +- src/soup_cli/commands/train.py | 2 +- src/soup_cli/commands/tui.py | 2 +- src/soup_cli/commands/ui.py | 2 +- src/soup_cli/config/schema.py | 36 ++--- src/soup_cli/eval/judge.py | 2 +- src/soup_cli/mcp_server/registry.py | 2 +- src/soup_cli/templates/audio.yaml | 2 +- src/soup_cli/templates/bco.yaml | 2 +- src/soup_cli/templates/chat.yaml | 2 +- src/soup_cli/templates/code.yaml | 2 +- src/soup_cli/templates/embedding.yaml | 2 +- src/soup_cli/templates/eu-ai-act.yaml | 4 +- src/soup_cli/templates/ipo.yaml | 2 +- src/soup_cli/templates/kto.yaml | 2 +- src/soup_cli/templates/longcontext.yaml | 4 +- src/soup_cli/templates/medical.yaml | 2 +- src/soup_cli/templates/moe.yaml | 2 +- src/soup_cli/templates/orpo.yaml | 2 +- src/soup_cli/templates/pretrain.yaml | 2 +- src/soup_cli/templates/reasoning.yaml | 2 +- src/soup_cli/templates/rlhf.yaml | 2 +- src/soup_cli/templates/simpo.yaml | 2 +- src/soup_cli/templates/tool-calling.yaml | 2 +- src/soup_cli/templates/vision.yaml | 2 +- src/soup_cli/trainer/mlx_dpo.py | 2 +- src/soup_cli/trainer/mlx_grpo.py | 2 +- src/soup_cli/trainer/mlx_sft.py | 2 +- src/soup_cli/trainer/online_dpo.py | 2 +- src/soup_cli/trainer/sft.py | 2 +- src/soup_cli/utils/apple_adapter.py | 2 +- src/soup_cli/utils/compile_tools.py | 4 +- src/soup_cli/utils/data_doctor.py | 2 +- src/soup_cli/utils/data_lint.py | 2 +- src/soup_cli/utils/delinearize_llama4.py | 2 +- src/soup_cli/utils/diagnose/_common.py | 2 +- src/soup_cli/utils/errors.py | 14 +- src/soup_cli/utils/grpo_long_context.py | 2 +- src/soup_cli/utils/liger.py | 2 +- src/soup_cli/utils/mii.py | 2 +- src/soup_cli/utils/prompt_compile.py | 2 +- src/soup_cli/utils/prune_prompt.py | 2 +- tests/test_v07136.py | 9 +- tests/test_v07137.py | 139 ++++++++++++++++++ 69 files changed, 341 insertions(+), 157 deletions(-) create mode 100644 tests/test_v07137.py diff --git a/CHANGELOG.md b/CHANGELOG.md index e0c4106..a1abb4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,8 +12,35 @@ reproducing 70+ versions of notes. ## [Unreleased] +## [0.71.37] - 2026-07-17 + +**Every `pip install soup-cli[extra]` command now works on Windows `cmd.exe`**, and +eval-gate benchmark tasks run instead of always failing. + ### Fixed +- **Install hints are now quoted so they work in every shell.** Soup printed + `pip install 'soup-cli[ui]'` — bash / zsh / PowerShell syntax. `cmd.exe` has no + single-quote quoting, so it hands the quotes to pip verbatim and pip refuses: + + ``` + ERROR: Invalid requirement: "'soup-cli[train]'": Expected package name at the start of dependency specifier + ``` + + Every hint, README command, and docs example now uses `pip install + "soup-cli[extra]"`, which works in cmd, PowerShell, bash, and zsh alike — the + same spelling the repo already used for `pip install -e ".[dev]"`. Measured on + Windows: single quotes fail only on `cmd.exe`; double quotes pass everywhere; + dropping the quotes passes on Windows but breaks zsh, which globs the bracket. + + Nothing in Soup can rescue the command after it is typed — pip and the shell + own it, and Soup is not installed yet when the README command runs — so the + fix is the spelling we print. A regression test now scans the package and every + docs code block for the single-quoted form. + + If you followed an older tutorial and hit `Invalid requirement`, swap the `'` + for `"`; nothing is wrong with the package. + - **Eval-gate `type: benchmark` tasks now actually run.** `eval/gate.py` probed for a `forgetting.run_mini_benchmark` helper that never existed, so every `type: benchmark` task in a gate suite failed 100% of the time — while diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 351e5c6..423dfa7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -120,7 +120,7 @@ src/soup_cli/ templates/ - 21 built-in soup.yaml templates (YAML + manifest.json) with load_template loader (v0.39.0, +bco v0.40.0, +4 compliance v0.71.35) ui/ - Web UI (FastAPI + HTML/JS SPA) -tests/ - Test suite (314 files, 16283 tests) +tests/ - Test suite (315 files, 16288 tests) examples/ - Real-world config examples and datasets ``` diff --git a/README.md b/README.md index c8e5536..7ffb9ab 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ Soup turns the pain of LLM fine-tuning into a simple workflow. One config, one command, done. ```bash -pip install 'soup-cli[train]' # add [train] to fine-tune; bare `soup-cli` is the light CLI +pip install "soup-cli[train]" # add [train] to fine-tune; bare `soup-cli` is the light CLI soup init --template chat soup train ``` @@ -68,7 +68,7 @@ infrastructure instead of improving models. Soup fixes that. separates them. The default is deliberately conservative; see the CHANGELOG. - **Two blocking bugs fixed:** the hardware-fit gate **refused to train any model you merged yourself** (a local checkpoint's name has no size marker, so it guessed 7B and predicted - 16 GB), and every `pip install 'soup-cli[extra]'` hint printed **without the extra** — + 16 GB), and every `pip install "soup-cli[extra]"` hint printed **without the extra** — 17 sites where the suggested command succeeds and leaves the feature still broken. ```bash @@ -104,26 +104,37 @@ Full history: [CHANGELOG.md](CHANGELOG.md) · [GitHub Releases](https://gi ### 1. Install ```bash -pip install soup-cli # light: CLI + config + data tools (no PyTorch) -pip install 'soup-cli[train]' # add the training stack (torch, transformers, peft, trl, …) -pip install git+https://github.com/MakazhanAlpamys/Soup.git # latest dev +# Light core: CLI + config + data tools, no PyTorch +pip install soup-cli + +# Add the training stack (torch, transformers, peft, trl, datasets, …) +pip install "soup-cli[train]" + +# Everything (train + serve + ui + data) in one shot +pip install "soup-cli[all]" + +# Or from GitHub (latest dev) +pip install git+https://github.com/MakazhanAlpamys/Soup.git ``` -> **On Windows `cmd.exe`, swap the single quotes for double quotes:** -> `pip install "soup-cli[train]"` +The full extras table (`fast`, `mlx`, `serve`, `eval`, `ui`, `vision`, `audio`, …) lives in +[`docs/models.md`](docs/models.md#optional-extras). + +> **Use double quotes around the extra.** They are the only spelling that works in +> every shell — `cmd.exe`, PowerShell, bash, and zsh. > -> The commands above are written for bash / zsh / PowerShell, where `'…'` quotes a -> string. `cmd.exe` has no single-quote quoting — it passes the characters straight -> to pip, which then rejects them: +> Older tutorials and videos (including some of ours) show the single-quoted +> `pip install 'soup-cli[train]'`. That is bash / zsh / PowerShell syntax, and it +> fails on Windows `cmd.exe`, which has no single-quote quoting and hands the +> quotes straight to pip: > > ``` > ERROR: Invalid requirement: "'soup-cli[train]'": Expected package name at the start of dependency specifier > ``` > -> That is pip parsing a literal `'`, not a problem with the package. Double quotes -> work in **every** shell — cmd, PowerShell, bash, and zsh — so `pip install -> "soup-cli[train]"` is always safe. (Unquoted `pip install soup-cli[train]` also -> works in cmd and PowerShell, but zsh reads `[train]` as a glob and fails.) +> If you hit that, swap the `'` for `"` — pip is rejecting a literal quote +> character, nothing is wrong with the package. (Dropping the quotes entirely +> works on Windows too, but zsh then reads `[train]` as a glob and fails.) `soup init`, `soup data …`, and the other data/inspection commands work on the light install. Fine-tuning (`soup train`) needs the `[train]` extra. @@ -159,7 +170,7 @@ A complete `soup.yaml`: ```yaml base: meta-llama/Llama-3.1-8B-Instruct task: sft -# backend: unsloth # 2-5x faster, pip install 'soup-cli[fast]' +# backend: unsloth # 2-5x faster, pip install "soup-cli[fast]" data: train: ./data/train.jsonl diff --git a/docs/backends-and-ops.md b/docs/backends-and-ops.md index 437ea28..f868bad 100644 --- a/docs/backends-and-ops.md +++ b/docs/backends-and-ops.md @@ -79,7 +79,7 @@ Fine-tune on M1-M4 Macs via Apple's [MLX](https://github.com/ml-explore/mlx) fra ```bash # Install MLX support -pip install 'soup-cli[mlx]' +pip install "soup-cli[mlx]" ``` ```yaml @@ -108,7 +108,7 @@ Use the [Unsloth](https://github.com/unslothai/unsloth) backend for significantl ```bash # Install unsloth support -pip install 'soup-cli[fast]' +pip install "soup-cli[fast]" ``` Then add one line to your config: @@ -143,7 +143,7 @@ app from your `soup.yaml` for serverless, per-second-billed GPU training. The co base64-embedded as **data** — no code interpolation, no secrets in the generated stub. ```bash -pip install 'soup-cli[modal]' # only needed for live submit +pip install "soup-cli[modal]" # only needed for live submit # Plan-only (default): write the stub + print the `modal run` command. soup train --config soup.yaml --cloud modal --gpu a100 @@ -585,7 +585,7 @@ Full-screen Textual dashboard. Two-pane: run list (left) + selected-run detail (right). `r` refreshes, `q` quits. ```bash -pip install 'soup-cli[tui]' +pip install "soup-cli[tui]" soup tui --refresh 1.0 --limit 50 ``` diff --git a/docs/commands.md b/docs/commands.md index 1bb3f21..cb75052 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -255,9 +255,9 @@ soup adapters bisect ... --eval-command "..." Binary search over training soup lock write --base-sha --dataset-sha --env-hash Write soup.lock (v0.67.0) soup lock write --base-sha --dataset-sha --env-lock soup-env.lock Auto-derive --env-hash from soup-env.lock (v0.71.1) soup lock show / soup lock check Show + drift-check (exit 3 on drift) -soup compile --eval [--optimizer mipro|gepa|textgrad|copro|bootstrap_fewshot] [--plan-only] DSPy / GEPA / TextGrad prompt-program compiler — live (v0.71.13; pip install 'soup-cli[compile]') +soup compile --eval [--optimizer mipro|gepa|textgrad|copro|bootstrap_fewshot] [--plan-only] DSPy / GEPA / TextGrad prompt-program compiler — live (v0.71.13; pip install "soup-cli[compile]") soup distill-prompt --traces --teacher --student --strategy sft|preference|kl [--provider ollama|anthropic|vllm] [--base-url ] [--temperature F] [--max-rows N] Distill prompt-heavy traces via a live teacher (v0.71.13) -soup compile-tools --eval [--optimizer textgrad|gepa] [--plan-only] TextGrad / GEPA tool-schema optimiser — live (v0.71.13; pip install 'soup-cli[compile]') +soup compile-tools --eval [--optimizer textgrad|gepa] [--plan-only] TextGrad / GEPA tool-schema optimiser — live (v0.71.13; pip install "soup-cli[compile]") soup apple-adapter --direction hf-to-mlx|mlx-to-hf|hf-to-apple|mlx-to-apple --output [--sign] [--plan-only] PEFT LoRA <-> mlx-lm adapter conversion — live (v0.71.21; *-to-apple upstream-gated exit 3) soup local-rl init --db Create personal-LLM flywheel SQLite schema (v0.68.0) soup local-rl status --db Print interactions / thumbs-up / thumbs-down counters @@ -299,7 +299,7 @@ server over **stdio**, so any MCP client — Claude Code, Cursor, Cline, Continu can drive Soup conversationally. Install the extra first: ```bash -pip install 'soup-cli[mcp]' +pip install "soup-cli[mcp]" ``` Register it with your client. For **Claude Code** (`.mcp.json` in the repo) or diff --git a/docs/data.md b/docs/data.md index d7363cf..1c5b709 100644 --- a/docs/data.md +++ b/docs/data.md @@ -337,7 +337,7 @@ soup data generate --prompt "..." --validate # Auto-filter by quality (coherence scoring) soup data generate --prompt "..." --filter -# Auto-dedup (MinHash, requires: pip install 'soup-cli[data]') +# Auto-dedup (MinHash, requires: pip install "soup-cli[data]") soup data generate --prompt "..." --dedup # Full quality pipeline: validate + filter + dedup @@ -591,7 +591,7 @@ soup data convert ./data/train.jsonl --to sharegpt --output converted.jsonl # Merge multiple datasets soup data merge data1.jsonl data2.jsonl --output merged.jsonl --shuffle -# Remove near-duplicates (requires: pip install 'soup-cli[data]') +# Remove near-duplicates (requires: pip install "soup-cli[data]") soup data dedup ./data/train.jsonl --threshold 0.8 # Extended statistics (length distribution, token counts, languages) @@ -814,7 +814,7 @@ Five checks: `length_bias` — the **#1 silent DPO degradation**: `chosen` systematically longer than `rejected`, reported as a Cohen's d effect size — `label_imbalance` (KTO desirable:undesirable ratio), `near_duplicates` (MinHash/LSH, reuses the `soup data dedup` kernel; requires -`pip install 'soup-cli[data]'`, degrades to an advisory skip otherwise), +`pip install "soup-cli[data]"`, degrades to an advisory skip otherwise), `identical_pairs` (`chosen == rejected` — zero preference signal), and `prompt_leak` (the prompt echoed verbatim inside the completion, a common synthetic-data pipeline bug). Same OK/MINOR/MAJOR taxonomy and exit codes as diff --git a/docs/evaluation.md b/docs/evaluation.md index 9f1bd5f..ce8f188 100644 --- a/docs/evaluation.md +++ b/docs/evaluation.md @@ -432,7 +432,7 @@ Full-featured evaluation platform with standard benchmarks, custom evals, LLM-as ```bash # Install eval dependencies -pip install 'soup-cli[eval]' +pip install "soup-cli[eval]" # Standard benchmarks (wraps lm-evaluation-harness) soup eval benchmark --model ./output --benchmarks mmlu,gsm8k,hellaswag diff --git a/docs/models.md b/docs/models.md index d26913b..64bfebf 100644 --- a/docs/models.md +++ b/docs/models.md @@ -70,29 +70,29 @@ no PyTorch. Add `[train]` to fine-tune, or install other extras only when you ne | Extra | Install | What it adds | |---|---|---| -| `train` | `pip install 'soup-cli[train]'` | Training stack: torch, transformers, peft, trl, datasets, bitsandbytes, accelerate | -| `all` | `pip install 'soup-cli[all]'` | `train` + `serve` + `ui` + `data` in one shot | -| `fast` | `pip install 'soup-cli[fast]'` | Unsloth backend (2-5x faster, lower VRAM) | -| `vision` | `pip install 'soup-cli[vision]'` | Vision / multimodal fine-tuning (Pillow) | -| `audio` | `pip install 'soup-cli[audio]'` | Audio / speech fine-tuning (librosa, soundfile) | -| `mlx` | `pip install 'soup-cli[mlx]'` | Apple Silicon backend (mlx, mlx-lm) | -| `qat` | `pip install 'soup-cli[qat]'` | Quantization-Aware Training (torchao) | -| `serve` | `pip install 'soup-cli[serve]'` | Inference server (FastAPI + uvicorn) | -| `serve-fast` | `pip install 'soup-cli[serve-fast]'` | vLLM inference backend (2-4x throughput) | -| `sglang` | `pip install 'soup-cli[sglang]'` | SGLang inference backend | -| `ui` | `pip install 'soup-cli[ui]'` | Web UI + inference server | -| `tui` | `pip install 'soup-cli[tui]'` | Full-screen Textual dashboard (`soup tui`) | -| `eval` | `pip install 'soup-cli[eval]'` | Benchmark evaluation (lm-evaluation-harness) | -| `data` | `pip install 'soup-cli[data]'` | Deduplication (MinHash via datasketch) | -| `data-pro` | `pip install 'soup-cli[data-pro]'` | Language detection + PII (langdetect, presidio) | -| `deepspeed` | `pip install 'soup-cli[deepspeed]'` | Multi-GPU training (DeepSpeed ZeRO) | -| `liger` | `pip install 'soup-cli[liger]'` | Liger Kernel fused ops | -| `ring-attn` | `pip install 'soup-cli[ring-attn]'` | Ring FlashAttention (sequence parallelism) | -| `onnx` / `tensorrt` | `pip install 'soup-cli[onnx]'` | ONNX / TensorRT-LLM export | -| `awq` / `gptq` | `pip install 'soup-cli[awq]'` | AWQ / GPTQ quantized export | -| `trackers` | `pip install 'soup-cli[trackers]'` | MLflow / SwanLab / Trackio logging | -| `remote` | `pip install 'soup-cli[remote]'` | Remote datasets (s3 / gs / az / oci) | -| `dev` | `pip install 'soup-cli[dev]'` | Tests + lint + types (pytest, ruff, mypy, pre-commit) | +| `train` | `pip install "soup-cli[train]"` | Training stack: torch, transformers, peft, trl, datasets, bitsandbytes, accelerate | +| `all` | `pip install "soup-cli[all]"` | `train` + `serve` + `ui` + `data` in one shot | +| `fast` | `pip install "soup-cli[fast]"` | Unsloth backend (2-5x faster, lower VRAM) | +| `vision` | `pip install "soup-cli[vision]"` | Vision / multimodal fine-tuning (Pillow) | +| `audio` | `pip install "soup-cli[audio]"` | Audio / speech fine-tuning (librosa, soundfile) | +| `mlx` | `pip install "soup-cli[mlx]"` | Apple Silicon backend (mlx, mlx-lm) | +| `qat` | `pip install "soup-cli[qat]"` | Quantization-Aware Training (torchao) | +| `serve` | `pip install "soup-cli[serve]"` | Inference server (FastAPI + uvicorn) | +| `serve-fast` | `pip install "soup-cli[serve-fast]"` | vLLM inference backend (2-4x throughput) | +| `sglang` | `pip install "soup-cli[sglang]"` | SGLang inference backend | +| `ui` | `pip install "soup-cli[ui]"` | Web UI + inference server | +| `tui` | `pip install "soup-cli[tui]"` | Full-screen Textual dashboard (`soup tui`) | +| `eval` | `pip install "soup-cli[eval]"` | Benchmark evaluation (lm-evaluation-harness) | +| `data` | `pip install "soup-cli[data]"` | Deduplication (MinHash via datasketch) | +| `data-pro` | `pip install "soup-cli[data-pro]"` | Language detection + PII (langdetect, presidio) | +| `deepspeed` | `pip install "soup-cli[deepspeed]"` | Multi-GPU training (DeepSpeed ZeRO) | +| `liger` | `pip install "soup-cli[liger]"` | Liger Kernel fused ops | +| `ring-attn` | `pip install "soup-cli[ring-attn]"` | Ring FlashAttention (sequence parallelism) | +| `onnx` / `tensorrt` | `pip install "soup-cli[onnx]"` | ONNX / TensorRT-LLM export | +| `awq` / `gptq` | `pip install "soup-cli[awq]"` | AWQ / GPTQ quantized export | +| `trackers` | `pip install "soup-cli[trackers]"` | MLflow / SwanLab / Trackio logging | +| `remote` | `pip install "soup-cli[remote]"` | Remote datasets (s3 / gs / az / oci) | +| `dev` | `pip install "soup-cli[dev]"` | Tests + lint + types (pytest, ruff, mypy, pre-commit) | The complete, authoritative extras list is in [`pyproject.toml`](../pyproject.toml). diff --git a/docs/performance-and-quantization.md b/docs/performance-and-quantization.md index 6f8c957..20aaba4 100644 --- a/docs/performance-and-quantization.md +++ b/docs/performance-and-quantization.md @@ -36,7 +36,7 @@ Train with simulated quantization for significantly better post-quantization qua ```bash # Install QAT support -pip install 'soup-cli[qat]' +pip install "soup-cli[qat]" ``` ```yaml @@ -71,7 +71,7 @@ QAT works with all training tasks (SFT, DPO, GRPO, PPO, KTO, ORPO, SimPO, IPO, P For H100 / H200 / B100 / B200 GPUs, train with float8 matmuls for ~2x speedup vs bf16 at comparable quality. This extends QAT infrastructure via `torchao.float8`: ```bash -pip install 'soup-cli[qat]' # torchao >= 0.5.0 includes torchao.float8 +pip install "soup-cli[qat]" # torchao >= 0.5.0 includes torchao.float8 ``` ```yaml @@ -106,7 +106,7 @@ Bool `true` stays on the int8 QAT path for backward compatibility. FP8 requires Models with 128k+ vocabularies (Llama 3.1, Qwen2) materialise a huge `(batch, seq, vocab)` logits tensor that dominates VRAM. Cut Cross-Entropy computes the loss in chunks instead: ```bash -pip install 'soup-cli[cce]' # or: pip install cut-cross-entropy +pip install "soup-cli[cce]" # or: pip install cut-cross-entropy ``` ```yaml @@ -368,9 +368,9 @@ data: Install optional performance packages: ```bash -pip install 'soup-cli[liger]' # Liger Kernel fused operations +pip install "soup-cli[liger]" # Liger Kernel fused operations pip install flash-attn --no-build-isolation # FlashAttention -pip install 'soup-cli[ring-attn]' # Ring FlashAttention (sequence parallelism) +pip install "soup-cli[ring-attn]" # Ring FlashAttention (sequence parallelism) ``` diff --git a/docs/serving-and-export.md b/docs/serving-and-export.md index 0935bc0..257c483 100644 --- a/docs/serving-and-export.md +++ b/docs/serving-and-export.md @@ -97,7 +97,7 @@ Linux/macOS need only a C++ toolchain + CMake. CUDA llama.cpp builds are unteste Export models to ONNX format for use with [ONNX Runtime](https://onnxruntime.ai/): ```bash -pip install 'soup-cli[onnx]' +pip install "soup-cli[onnx]" soup export --model ./output --format onnx soup export --model ./output --format onnx --output ./model_onnx ``` @@ -107,7 +107,7 @@ soup export --model ./output --format onnx --output ./model_onnx Export models to TensorRT-LLM format for high-throughput GPU inference: ```bash -pip install 'soup-cli[tensorrt]' +pip install "soup-cli[tensorrt]" soup export --model ./output --format tensorrt soup export --model ./output --format tensorrt --output ./model_trt ``` @@ -212,7 +212,7 @@ Start a local OpenAI-compatible inference server: ```bash # Install server dependencies -pip install 'soup-cli[serve]' +pip install "soup-cli[serve]" # Start server soup serve --model ./output --port 8000 @@ -253,7 +253,7 @@ Use [vLLM](https://github.com/vllm-project/vllm) for significantly better throug ```bash # Install vLLM support -pip install 'soup-cli[serve-fast]' +pip install "soup-cli[serve-fast]" # Start with vLLM backend soup serve --model ./output --backend vllm @@ -273,7 +273,7 @@ Use [SGLang](https://github.com/sgl-project/sglang) as an alternative high-throu ```bash # Install SGLang support -pip install 'soup-cli[sglang]' +pip install "soup-cli[sglang]" # Start with SGLang backend soup serve --model ./output --backend sglang @@ -482,7 +482,7 @@ The OTLP endpoint is SSRF-hardened: only http/https schemes, plain HTTP only for Launch a local web interface to manage experiments, start training, explore data, and chat with models — all from your browser. ```bash -pip install 'soup-cli[ui]' +pip install "soup-cli[ui]" soup ui # -> opens http://127.0.0.1:7860 in your browser # -> prints auth token to console diff --git a/docs/training.md b/docs/training.md index e722d85..1cdd275 100644 --- a/docs/training.md +++ b/docs/training.md @@ -531,7 +531,7 @@ Fine-tune vision-language models (LLaMA-3.2-Vision, Qwen2-VL, Pixtral) on image+ ```bash # Install vision support -pip install 'soup-cli[vision]' +pip install "soup-cli[vision]" # Create a vision config soup init --template vision @@ -581,7 +581,7 @@ Fine-tune audio-language models (Qwen2-Audio, Whisper) on audio+text data: ```bash # Install audio support -pip install 'soup-cli[audio]' +pip install "soup-cli[audio]" # Create an audio config soup init --template audio diff --git a/examples/README.md b/examples/README.md index ea7fdfe..163a4e7 100644 --- a/examples/README.md +++ b/examples/README.md @@ -137,7 +137,7 @@ soup train - Uses RoPE scaling (dynamic) to extend context to 128k tokens - Enables gradient checkpointing and FlashAttention for memory efficiency - Supports `linear`, `dynamic`, `yarn`, `longrope` scaling types -- Optional Liger Kernel for fused ops: `pip install 'soup-cli[liger]'` +- Optional Liger Kernel for fused ops: `pip install "soup-cli[liger]"` ### 9. Embedding Model Fine-Tuning @@ -159,7 +159,7 @@ soup train Fine-tune audio-language models (Qwen2-Audio, Whisper): ```bash -pip install 'soup-cli[audio]' +pip install "soup-cli[audio]" soup init --template audio soup train ``` @@ -301,7 +301,7 @@ quantization: int8 # Reduces memory by 4x Unsloth is 2-5x faster training: ```bash -pip install 'soup-cli[fast]' +pip install "soup-cli[fast]" ``` Then in your config: diff --git a/examples/configs/dpo_example.yaml b/examples/configs/dpo_example.yaml index 6e94dcc..242f16b 100644 --- a/examples/configs/dpo_example.yaml +++ b/examples/configs/dpo_example.yaml @@ -7,7 +7,7 @@ base: meta-llama/Llama-3.1-8B-Instruct task: dpo -# backend: unsloth # 2-5x faster, pip install 'soup-cli[fast]' +# backend: unsloth # 2-5x faster, pip install "soup-cli[fast]" data: train: examples/data/dpo_sample.jsonl @@ -32,7 +32,7 @@ training: # gradient_checkpointing: true # Save memory on long sequences # packing: true # Pack short samples for faster training # use_flash_attn: true # FlashAttention for faster attention - # use_liger: true # Liger Kernel fused ops, pip install 'soup-cli[liger]' + # use_liger: true # Liger Kernel fused ops, pip install "soup-cli[liger]" lora: r: 16 alpha: 32 diff --git a/pyproject.toml b/pyproject.toml index 4eb5882..867a30b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "soup-cli" -version = "0.71.36" +version = "0.71.37" description = "Fine-tune and post-train LLMs in one command. No SSH, no config hell." readme = "README.md" license = "Apache-2.0" diff --git a/src/soup_cli/__init__.py b/src/soup_cli/__init__.py index ab712b1..a115a12 100644 --- a/src/soup_cli/__init__.py +++ b/src/soup_cli/__init__.py @@ -1,3 +1,3 @@ """Soup CLI — Fine-tune and post-train LLMs in one command.""" -__version__ = "0.71.36" +__version__ = "0.71.37" diff --git a/src/soup_cli/cloud/modal.py b/src/soup_cli/cloud/modal.py index ed59bd9..47a3f4a 100644 --- a/src/soup_cli/cloud/modal.py +++ b/src/soup_cli/cloud/modal.py @@ -280,7 +280,7 @@ def submit_modal_run(plan: CloudPlan, *, env: Optional[Mapping] = None) -> int: import modal # noqa: F401 — presence check only except ImportError as exc: raise RuntimeError( - "Modal SDK not installed. Run `pip install 'soup-cli[modal]'`." + "Modal SDK not installed. Run `pip install \"soup-cli[modal]\"`." ) from exc import subprocess diff --git a/src/soup_cli/commands/compile_cmd.py b/src/soup_cli/commands/compile_cmd.py index 1993a9b..23a7afa 100644 --- a/src/soup_cli/commands/compile_cmd.py +++ b/src/soup_cli/commands/compile_cmd.py @@ -2,7 +2,7 @@ Renders a ``CompilePlan`` panel and, when ``--plan-only`` is omitted, runs the live optimizer (v0.71.13 #225). A missing optimizer library surfaces a -friendly exit-2 advisory naming ``pip install 'soup-cli[compile]'``. +friendly exit-2 advisory naming ``pip install "soup-cli[compile]"``. """ from __future__ import annotations diff --git a/src/soup_cli/commands/data.py b/src/soup_cli/commands/data.py index 3dd4267..ebc00bb 100644 --- a/src/soup_cli/commands/data.py +++ b/src/soup_cli/commands/data.py @@ -264,9 +264,11 @@ def _semantic_dedup( console.print( "[red]Semantic dedup needs PyTorch + transformers.[/]\n" # \[train] is escaped: Rich would otherwise eat the bracket as a - # markup tag and print `pip install 'soup-cli'` -- a command that + # markup tag and print `pip install "soup-cli"` -- a command that # installs the package WITHOUT the extra the user is missing. - "Install with: [bold]pip install 'soup-cli\\[train]'[/]" + # Double quotes, not single: cmd.exe cannot strip `'` and pip then + # rejects the requirement outright. + "Install with: [bold]pip install \"soup-cli\\[train]\"[/]" ) raise typer.Exit(1) except (ValueError, TypeError) as exc: @@ -358,7 +360,7 @@ def dedup( except ImportError: console.print( "[red]datasketch not installed.[/]\n" - "Install with: [bold]pip install 'soup-cli\\[data]'[/]" + "Install with: [bold]pip install \"soup-cli\\[data]\"[/]" ) raise typer.Exit(1) diff --git a/src/soup_cli/commands/data_canary.py b/src/soup_cli/commands/data_canary.py index 3a0f792..636936f 100644 --- a/src/soup_cli/commands/data_canary.py +++ b/src/soup_cli/commands/data_canary.py @@ -191,7 +191,7 @@ def check( "[red]soup data canary check needs PyTorch + transformers.[/]\n" # \[train] escaped: Rich would eat the bracket and print a # command that installs WITHOUT the extra. - "Install with: [bold]pip install 'soup-cli\\[train]'[/]" + "Install with: [bold]pip install \"soup-cli\\[train]\"[/]" ) raise typer.Exit(1) except (ValueError, OSError, RuntimeError) as exc: diff --git a/src/soup_cli/commands/data_topics.py b/src/soup_cli/commands/data_topics.py index 4cc3dfd..655adde 100644 --- a/src/soup_cli/commands/data_topics.py +++ b/src/soup_cli/commands/data_topics.py @@ -117,7 +117,7 @@ def topics( "[red]soup data topics needs PyTorch + transformers.[/]\n" # \[train] escaped: Rich would eat the bracket and print a # command that installs WITHOUT the extra. - "Install with: [bold]pip install 'soup-cli\\[train]'[/]" + "Install with: [bold]pip install \"soup-cli\\[train]\"[/]" ) raise typer.Exit(1) except (ValueError, TypeError) as exc: diff --git a/src/soup_cli/commands/eval.py b/src/soup_cli/commands/eval.py index c0ccbbe..712663d 100644 --- a/src/soup_cli/commands/eval.py +++ b/src/soup_cli/commands/eval.py @@ -108,7 +108,7 @@ def benchmark( except ImportError: console.print( "[red]lm-eval not installed.[/]\n" - "Install with: [bold]pip install 'soup-cli\\[eval]'[/]" + "Install with: [bold]pip install \"soup-cli\\[eval]\"[/]" ) raise typer.Exit(1) diff --git a/src/soup_cli/commands/export.py b/src/soup_cli/commands/export.py index a43c88b..c1e421c 100644 --- a/src/soup_cli/commands/export.py +++ b/src/soup_cli/commands/export.py @@ -579,7 +579,7 @@ def _export_onnx( except ImportError: console.print( "[red]optimum not installed.[/]\n" - "Install with: [bold]pip install 'soup-cli\\[onnx]'[/]\n" + "Install with: [bold]pip install \"soup-cli\\[onnx]\"[/]\n" "Or directly: [bold]pip install optimum[onnx][/]" ) raise typer.Exit(1) @@ -664,7 +664,7 @@ def _export_tensorrt( if not trtllm_available: console.print( "[red]tensorrt_llm not installed.[/]\n" - "Install with: [bold]pip install 'soup-cli\\[tensorrt]'[/]\n" + "Install with: [bold]pip install \"soup-cli\\[tensorrt]\"[/]\n" "Or follow: https://github.com/NVIDIA/TensorRT-LLM#installation" ) raise typer.Exit(1) @@ -857,7 +857,7 @@ def _export_awq( except ImportError: console.print( "[red]autoawq not installed.[/]\n" - "Install with: [bold]pip install 'soup-cli\\[awq]'[/]\n" + "Install with: [bold]pip install \"soup-cli\\[awq]\"[/]\n" "Or directly: [bold]pip install autoawq[/]" ) raise typer.Exit(1) @@ -989,7 +989,7 @@ def _export_gptq( except ImportError: console.print( "[red]auto-gptq not installed.[/]\n" - "Install with: [bold]pip install 'soup-cli\\[gptq]'[/]\n" + "Install with: [bold]pip install \"soup-cli\\[gptq]\"[/]\n" "Or directly: [bold]pip install auto-gptq[/]" ) raise typer.Exit(1) diff --git a/src/soup_cli/commands/generate.py b/src/soup_cli/commands/generate.py index 46c28da..a12ddaa 100644 --- a/src/soup_cli/commands/generate.py +++ b/src/soup_cli/commands/generate.py @@ -487,7 +487,7 @@ def _run_dedup_pipeline(path: Path) -> None: except ImportError: console.print( "[yellow]Dedup: datasketch not installed, skipping. " - "Install: pip install 'soup-cli\\[data]'[/]" + "Install: pip install \"soup-cli\\[data]\"[/]" ) diff --git a/src/soup_cli/commands/mcp.py b/src/soup_cli/commands/mcp.py index 498bae8..8689516 100644 --- a/src/soup_cli/commands/mcp.py +++ b/src/soup_cli/commands/mcp.py @@ -51,7 +51,7 @@ def serve( # instead of parsing '[mcp]' as a (dropped) markup tag. console.print( "[red]The MCP server needs the 'mcp' SDK.[/] " - "Install it with: [bold]pip install 'soup-cli\\[mcp]'[/]" + "Install it with: [bold]pip install \"soup-cli\\[mcp]\"[/]" ) raise typer.Exit(1) from None diff --git a/src/soup_cli/commands/merge_sharded_fsdp_weights.py b/src/soup_cli/commands/merge_sharded_fsdp_weights.py index 5906a91..5ebc12e 100644 --- a/src/soup_cli/commands/merge_sharded_fsdp_weights.py +++ b/src/soup_cli/commands/merge_sharded_fsdp_weights.py @@ -70,7 +70,7 @@ def merge_sharded_fsdp_weights( except ImportError as exc: console.print( "[red]torch + safetensors are required for consolidation. " - "Install with: [bold]pip install 'soup-cli\\[train]'[/][/]" + "Install with: [bold]pip install \"soup-cli\\[train]\"[/][/]" ) raise typer.Exit(code=1) from exc diff --git a/src/soup_cli/commands/serve.py b/src/soup_cli/commands/serve.py index a9b3930..90ec7fc 100644 --- a/src/soup_cli/commands/serve.py +++ b/src/soup_cli/commands/serve.py @@ -460,7 +460,7 @@ def serve( except ImportError: console.print( "[red]FastAPI/uvicorn not installed.[/]\n" - "Install with: [bold]pip install 'soup-cli\\[serve]'[/]" + "Install with: [bold]pip install \"soup-cli\\[serve]\"[/]" ) raise typer.Exit(1) @@ -537,7 +537,7 @@ def serve( if not is_vllm_available(): console.print( "[red]vLLM not installed.[/]\n" - "Install with: [bold]pip install 'soup-cli\\[serve-fast]'[/]" + "Install with: [bold]pip install \"soup-cli\\[serve-fast]\"[/]" ) raise typer.Exit(1) @@ -548,7 +548,7 @@ def serve( if not check_sglang_available(): console.print( "[red]SGLang not installed.[/]\n" - "Install with: [bold]pip install 'soup-cli\\[sglang]'[/]" + "Install with: [bold]pip install \"soup-cli\\[sglang]\"[/]" ) raise typer.Exit(1) diff --git a/src/soup_cli/commands/train.py b/src/soup_cli/commands/train.py index 380c43e..693b298 100644 --- a/src/soup_cli/commands/train.py +++ b/src/soup_cli/commands/train.py @@ -767,7 +767,7 @@ def train( except ImportError: console.print( "[red]wandb not installed.[/]\n" - "Run: [bold]pip install 'soup-cli\\[wandb]'[/]" + "Run: [bold]pip install \"soup-cli\\[wandb]\"[/]" ) raise typer.Exit(1) except Exception as wandb_err: diff --git a/src/soup_cli/commands/tui.py b/src/soup_cli/commands/tui.py index 1c9c66b..545dce7 100644 --- a/src/soup_cli/commands/tui.py +++ b/src/soup_cli/commands/tui.py @@ -17,7 +17,7 @@ console = Console() def _missing_dep_panel() -> str: return ( "[red]Textual is not installed.[/]\n\n" - "[bold]Install:[/] pip install 'soup-cli\\[tui]'\n" + "[bold]Install:[/] pip install \"soup-cli\\[tui]\"\n" "[dim]Or directly:[/] pip install textual" ) diff --git a/src/soup_cli/commands/ui.py b/src/soup_cli/commands/ui.py index 69a90b1..b0190ba 100644 --- a/src/soup_cli/commands/ui.py +++ b/src/soup_cli/commands/ui.py @@ -68,7 +68,7 @@ def ui( except ImportError: console.print( "[red]FastAPI/uvicorn not installed.[/]\n" - "Install with: [bold]pip install 'soup-cli\\[ui]'[/]" + "Install with: [bold]pip install \"soup-cli\\[ui]\"[/]" ) raise typer.Exit(1) diff --git a/src/soup_cli/config/schema.py b/src/soup_cli/config/schema.py index 358db17..947cd45 100644 --- a/src/soup_cli/config/schema.py +++ b/src/soup_cli/config/schema.py @@ -5554,7 +5554,7 @@ TEMPLATES: dict[str, str] = { base: meta-llama/Llama-3.1-8B-Instruct task: sft -# backend: unsloth # 2-5x faster, pip install 'soup-cli[fast]' +# backend: unsloth # 2-5x faster, pip install "soup-cli[fast]" data: train: ./data/train.jsonl @@ -5579,7 +5579,7 @@ output: ./output base: codellama/CodeLlama-7b-Instruct-hf task: sft -# backend: unsloth # 2-5x faster, pip install 'soup-cli[fast]' +# backend: unsloth # 2-5x faster, pip install "soup-cli[fast]" data: train: ./data/code_train.jsonl @@ -5604,7 +5604,7 @@ output: ./output base: meta-llama/Llama-3.1-8B-Instruct task: grpo -# backend: unsloth # 2-5x faster, pip install 'soup-cli[fast]' +# backend: unsloth # 2-5x faster, pip install "soup-cli[fast]" data: train: ./data/reasoning_train.jsonl @@ -5634,7 +5634,7 @@ output: ./output base: meta-llama/Llama-3.2-11B-Vision-Instruct task: sft modality: vision -# backend: unsloth # 2-5x faster, pip install 'soup-cli[fast]' +# backend: unsloth # 2-5x faster, pip install "soup-cli[fast]" data: train: ./data/vision_train.jsonl @@ -5660,7 +5660,7 @@ output: ./output base: meta-llama/Llama-3.1-8B-Instruct task: sft -# backend: unsloth # 2-5x faster, pip install 'soup-cli[fast]' +# backend: unsloth # 2-5x faster, pip install "soup-cli[fast]" data: train: ./data/medical_train.jsonl @@ -5690,7 +5690,7 @@ output: ./output base: meta-llama/Llama-3.1-8B-Instruct task: kto -# backend: unsloth # 2-5x faster, pip install 'soup-cli[fast]' +# backend: unsloth # 2-5x faster, pip install "soup-cli[fast]" data: train: ./data/kto_train.jsonl @@ -5720,7 +5720,7 @@ output: ./output base: meta-llama/Llama-3.1-8B-Instruct task: orpo -# backend: unsloth # 2-5x faster, pip install 'soup-cli[fast]' +# backend: unsloth # 2-5x faster, pip install "soup-cli[fast]" data: train: ./data/preference_train.jsonl @@ -5750,7 +5750,7 @@ output: ./output base: meta-llama/Llama-3.1-8B-Instruct task: bco -# backend: unsloth # 2-5x faster, pip install 'soup-cli[fast]' +# backend: unsloth # 2-5x faster, pip install "soup-cli[fast]" data: train: ./data/preference_train.jsonl @@ -5780,7 +5780,7 @@ output: ./output base: meta-llama/Llama-3.1-8B-Instruct task: simpo -# backend: unsloth # 2-5x faster, pip install 'soup-cli[fast]' +# backend: unsloth # 2-5x faster, pip install "soup-cli[fast]" data: train: ./data/preference_train.jsonl @@ -5811,7 +5811,7 @@ output: ./output base: meta-llama/Llama-3.1-8B-Instruct task: ipo -# backend: unsloth # 2-5x faster, pip install 'soup-cli[fast]' +# backend: unsloth # 2-5x faster, pip install "soup-cli[fast]" data: train: ./data/preference_train.jsonl @@ -5843,7 +5843,7 @@ output: ./output base: meta-llama/Llama-3.1-8B task: pretrain -# backend: unsloth # 2-5x faster, pip install 'soup-cli[fast]' +# backend: unsloth # 2-5x faster, pip install "soup-cli[fast]" data: train: ./data/corpus.jsonl @@ -5871,7 +5871,7 @@ output: ./output_pretrain base: Qwen/Qwen3-30B-A3B task: sft -# backend: unsloth # 2-5x faster, pip install 'soup-cli[fast]' +# backend: unsloth # 2-5x faster, pip install "soup-cli[fast]" data: train: ./data/train.jsonl @@ -5902,7 +5902,7 @@ output: ./output base: meta-llama/Llama-3.1-8B-Instruct task: sft -# backend: unsloth # 2-5x faster, pip install 'soup-cli[fast]' +# backend: unsloth # 2-5x faster, pip install "soup-cli[fast]" data: train: ./data/long_context_train.jsonl @@ -5923,7 +5923,7 @@ training: gradient_checkpointing: true rope_scaling_type: dynamic use_flash_attn: true - # use_liger: true # pip install 'soup-cli[liger]' for fused ops + # use_liger: true # pip install "soup-cli[liger]" for fused ops # use_ring_attention: true # Multi-GPU sequence parallelism output: ./output_longctx @@ -5939,7 +5939,7 @@ output: ./output_longctx base: BAAI/bge-base-en-v1.5 task: embedding -# backend: unsloth # 2-5x faster, pip install 'soup-cli[fast]' +# backend: unsloth # 2-5x faster, pip install "soup-cli[fast]" data: train: ./data/embedding_train.jsonl @@ -5976,7 +5976,7 @@ output: ./output_embedding base: Qwen/Qwen2-Audio-7B-Instruct task: sft modality: audio -# backend: unsloth # 2-5x faster, pip install 'soup-cli[fast]' +# backend: unsloth # 2-5x faster, pip install "soup-cli[fast]" data: train: ./data/audio_train.jsonl @@ -6017,7 +6017,7 @@ output: ./output_audio base: meta-llama/Llama-3.1-8B-Instruct task: sft -# backend: unsloth # 2-5x faster, pip install 'soup-cli[fast]' +# backend: unsloth # 2-5x faster, pip install "soup-cli[fast]" data: train: ./data/tool_calling_train.jsonl @@ -6051,7 +6051,7 @@ output: ./output base: meta-llama/Llama-3.1-8B-Instruct task: ppo -# backend: unsloth # 2-5x faster, pip install 'soup-cli[fast]' +# backend: unsloth # 2-5x faster, pip install "soup-cli[fast]" data: train: ./data/prompts.jsonl diff --git a/src/soup_cli/eval/judge.py b/src/soup_cli/eval/judge.py index e799c06..dd4e2c2 100644 --- a/src/soup_cli/eval/judge.py +++ b/src/soup_cli/eval/judge.py @@ -518,7 +518,7 @@ def _base_pairwise_judge_cls() -> type: from trl import BasePairwiseJudge except ImportError as exc: # pragma: no cover — trl ships in [train]/[dev] raise ImportError( - "SoupPairwiseJudge needs trl>=0.19 (pip install 'soup-cli[train]')" + "SoupPairwiseJudge needs trl>=0.19 (pip install \"soup-cli[train]\")" ) from exc return BasePairwiseJudge diff --git a/src/soup_cli/mcp_server/registry.py b/src/soup_cli/mcp_server/registry.py index d60022c..18ca4fa 100644 --- a/src/soup_cli/mcp_server/registry.py +++ b/src/soup_cli/mcp_server/registry.py @@ -269,7 +269,7 @@ def tool_data_doctor(args: dict) -> dict: import transformers # noqa: F401 except ImportError as exc: raise McpToolError( - "data_doctor needs the tokenizer stack: pip install 'soup-cli[train]'" + "data_doctor needs the tokenizer stack: pip install \"soup-cli[train]\"" ) from exc try: tok = _dd.resolve_tokenizer(model, trust_remote_code=False) diff --git a/src/soup_cli/templates/audio.yaml b/src/soup_cli/templates/audio.yaml index 084a90d..0dd1119 100644 --- a/src/soup_cli/templates/audio.yaml +++ b/src/soup_cli/templates/audio.yaml @@ -11,7 +11,7 @@ base: Qwen/Qwen2-Audio-7B-Instruct task: sft modality: audio -# backend: unsloth # 2-5x faster, pip install 'soup-cli[fast]' +# backend: unsloth # 2-5x faster, pip install "soup-cli[fast]" data: train: ./data/audio_train.jsonl diff --git a/src/soup_cli/templates/bco.yaml b/src/soup_cli/templates/bco.yaml index a166953..ccf9a78 100644 --- a/src/soup_cli/templates/bco.yaml +++ b/src/soup_cli/templates/bco.yaml @@ -6,7 +6,7 @@ base: meta-llama/Llama-3.1-8B-Instruct task: bco -# backend: unsloth # 2-5x faster, pip install 'soup-cli[fast]' +# backend: unsloth # 2-5x faster, pip install "soup-cli[fast]" data: train: ./data/preference_train.jsonl diff --git a/src/soup_cli/templates/chat.yaml b/src/soup_cli/templates/chat.yaml index b13b5d5..743ee99 100644 --- a/src/soup_cli/templates/chat.yaml +++ b/src/soup_cli/templates/chat.yaml @@ -3,7 +3,7 @@ base: meta-llama/Llama-3.1-8B-Instruct task: sft -# backend: unsloth # 2-5x faster, pip install 'soup-cli[fast]' +# backend: unsloth # 2-5x faster, pip install "soup-cli[fast]" data: train: ./data/train.jsonl diff --git a/src/soup_cli/templates/code.yaml b/src/soup_cli/templates/code.yaml index 1ef92fd..fb7d461 100644 --- a/src/soup_cli/templates/code.yaml +++ b/src/soup_cli/templates/code.yaml @@ -3,7 +3,7 @@ base: codellama/CodeLlama-7b-Instruct-hf task: sft -# backend: unsloth # 2-5x faster, pip install 'soup-cli[fast]' +# backend: unsloth # 2-5x faster, pip install "soup-cli[fast]" data: train: ./data/code_train.jsonl diff --git a/src/soup_cli/templates/embedding.yaml b/src/soup_cli/templates/embedding.yaml index 471c724..b4b3884 100644 --- a/src/soup_cli/templates/embedding.yaml +++ b/src/soup_cli/templates/embedding.yaml @@ -9,7 +9,7 @@ base: BAAI/bge-base-en-v1.5 task: embedding -# backend: unsloth # 2-5x faster, pip install 'soup-cli[fast]' +# backend: unsloth # 2-5x faster, pip install "soup-cli[fast]" data: train: ./data/embedding_train.jsonl diff --git a/src/soup_cli/templates/eu-ai-act.yaml b/src/soup_cli/templates/eu-ai-act.yaml index 8423973..dfe0996 100644 --- a/src/soup_cli/templates/eu-ai-act.yaml +++ b/src/soup_cli/templates/eu-ai-act.yaml @@ -10,8 +10,8 @@ # # 2. Auto-generate the Annex XI/XII technical documentation + measure energy: # soup train --config soup.yaml \ -# --annex-xi annex_xi.md \ # or annex_xi.pdf (pip install 'soup-cli[pdf]') -# --track-energy --energy-country DEU \ # codecarbon (pip install 'soup-cli[carbon]') +# --annex-xi annex_xi.md \ # or annex_xi.pdf (pip install "soup-cli[pdf]") +# --track-energy --energy-country DEU \ # codecarbon (pip install "soup-cli[carbon]") # --energy-out energy.json # # 3. Fold the measured energy into the ML-BOM (transparency): diff --git a/src/soup_cli/templates/ipo.yaml b/src/soup_cli/templates/ipo.yaml index 2329391..1b94409 100644 --- a/src/soup_cli/templates/ipo.yaml +++ b/src/soup_cli/templates/ipo.yaml @@ -6,7 +6,7 @@ base: meta-llama/Llama-3.1-8B-Instruct task: ipo -# backend: unsloth # 2-5x faster, pip install 'soup-cli[fast]' +# backend: unsloth # 2-5x faster, pip install "soup-cli[fast]" data: train: ./data/preference_train.jsonl diff --git a/src/soup_cli/templates/kto.yaml b/src/soup_cli/templates/kto.yaml index 8bacda3..e482b12 100644 --- a/src/soup_cli/templates/kto.yaml +++ b/src/soup_cli/templates/kto.yaml @@ -7,7 +7,7 @@ base: meta-llama/Llama-3.1-8B-Instruct task: kto -# backend: unsloth # 2-5x faster, pip install 'soup-cli[fast]' +# backend: unsloth # 2-5x faster, pip install "soup-cli[fast]" data: train: ./data/kto_train.jsonl diff --git a/src/soup_cli/templates/longcontext.yaml b/src/soup_cli/templates/longcontext.yaml index 51251e4..bc35adc 100644 --- a/src/soup_cli/templates/longcontext.yaml +++ b/src/soup_cli/templates/longcontext.yaml @@ -6,7 +6,7 @@ base: meta-llama/Llama-3.1-8B-Instruct task: sft -# backend: unsloth # 2-5x faster, pip install 'soup-cli[fast]' +# backend: unsloth # 2-5x faster, pip install "soup-cli[fast]" data: train: ./data/long_context_train.jsonl @@ -27,7 +27,7 @@ training: gradient_checkpointing: true rope_scaling_type: dynamic use_flash_attn: true - # use_liger: true # pip install 'soup-cli[liger]' for fused ops + # use_liger: true # pip install "soup-cli[liger]" for fused ops # use_ring_attention: true # Multi-GPU sequence parallelism output: ./output_longctx diff --git a/src/soup_cli/templates/medical.yaml b/src/soup_cli/templates/medical.yaml index f78fcc1..0172d6b 100644 --- a/src/soup_cli/templates/medical.yaml +++ b/src/soup_cli/templates/medical.yaml @@ -3,7 +3,7 @@ base: meta-llama/Llama-3.1-8B-Instruct task: sft -# backend: unsloth # 2-5x faster, pip install 'soup-cli[fast]' +# backend: unsloth # 2-5x faster, pip install "soup-cli[fast]" data: train: ./data/medical_train.jsonl diff --git a/src/soup_cli/templates/moe.yaml b/src/soup_cli/templates/moe.yaml index 0270d34..bb2eba8 100644 --- a/src/soup_cli/templates/moe.yaml +++ b/src/soup_cli/templates/moe.yaml @@ -5,7 +5,7 @@ base: Qwen/Qwen3-30B-A3B task: sft -# backend: unsloth # 2-5x faster, pip install 'soup-cli[fast]' +# backend: unsloth # 2-5x faster, pip install "soup-cli[fast]" data: train: ./data/train.jsonl diff --git a/src/soup_cli/templates/orpo.yaml b/src/soup_cli/templates/orpo.yaml index 191ed1f..48d395e 100644 --- a/src/soup_cli/templates/orpo.yaml +++ b/src/soup_cli/templates/orpo.yaml @@ -6,7 +6,7 @@ base: meta-llama/Llama-3.1-8B-Instruct task: orpo -# backend: unsloth # 2-5x faster, pip install 'soup-cli[fast]' +# backend: unsloth # 2-5x faster, pip install "soup-cli[fast]" data: train: ./data/preference_train.jsonl diff --git a/src/soup_cli/templates/pretrain.yaml b/src/soup_cli/templates/pretrain.yaml index 8ea6a7f..327aa95 100644 --- a/src/soup_cli/templates/pretrain.yaml +++ b/src/soup_cli/templates/pretrain.yaml @@ -8,7 +8,7 @@ base: meta-llama/Llama-3.1-8B task: pretrain -# backend: unsloth # 2-5x faster, pip install 'soup-cli[fast]' +# backend: unsloth # 2-5x faster, pip install "soup-cli[fast]" data: train: ./data/corpus.jsonl diff --git a/src/soup_cli/templates/reasoning.yaml b/src/soup_cli/templates/reasoning.yaml index 97eb423..fb5ecee 100644 --- a/src/soup_cli/templates/reasoning.yaml +++ b/src/soup_cli/templates/reasoning.yaml @@ -3,7 +3,7 @@ base: meta-llama/Llama-3.1-8B-Instruct task: grpo -# backend: unsloth # 2-5x faster, pip install 'soup-cli[fast]' +# backend: unsloth # 2-5x faster, pip install "soup-cli[fast]" data: train: ./data/reasoning_train.jsonl diff --git a/src/soup_cli/templates/rlhf.yaml b/src/soup_cli/templates/rlhf.yaml index 29002d2..0583cd5 100644 --- a/src/soup_cli/templates/rlhf.yaml +++ b/src/soup_cli/templates/rlhf.yaml @@ -11,7 +11,7 @@ base: meta-llama/Llama-3.1-8B-Instruct task: ppo -# backend: unsloth # 2-5x faster, pip install 'soup-cli[fast]' +# backend: unsloth # 2-5x faster, pip install "soup-cli[fast]" data: train: ./data/prompts.jsonl diff --git a/src/soup_cli/templates/simpo.yaml b/src/soup_cli/templates/simpo.yaml index 90aed3a..78170c0 100644 --- a/src/soup_cli/templates/simpo.yaml +++ b/src/soup_cli/templates/simpo.yaml @@ -6,7 +6,7 @@ base: meta-llama/Llama-3.1-8B-Instruct task: simpo -# backend: unsloth # 2-5x faster, pip install 'soup-cli[fast]' +# backend: unsloth # 2-5x faster, pip install "soup-cli[fast]" data: train: ./data/preference_train.jsonl diff --git a/src/soup_cli/templates/tool-calling.yaml b/src/soup_cli/templates/tool-calling.yaml index e874e46..9eba6b8 100644 --- a/src/soup_cli/templates/tool-calling.yaml +++ b/src/soup_cli/templates/tool-calling.yaml @@ -17,7 +17,7 @@ base: meta-llama/Llama-3.1-8B-Instruct task: sft -# backend: unsloth # 2-5x faster, pip install 'soup-cli[fast]' +# backend: unsloth # 2-5x faster, pip install "soup-cli[fast]" data: train: ./data/tool_calling_train.jsonl diff --git a/src/soup_cli/templates/vision.yaml b/src/soup_cli/templates/vision.yaml index 367f86d..84a1a4f 100644 --- a/src/soup_cli/templates/vision.yaml +++ b/src/soup_cli/templates/vision.yaml @@ -4,7 +4,7 @@ base: meta-llama/Llama-3.2-11B-Vision-Instruct task: sft modality: vision -# backend: unsloth # 2-5x faster, pip install 'soup-cli[fast]' +# backend: unsloth # 2-5x faster, pip install "soup-cli[fast]" data: train: ./data/vision_train.jsonl diff --git a/src/soup_cli/trainer/mlx_dpo.py b/src/soup_cli/trainer/mlx_dpo.py index bba2e98..99365f2 100644 --- a/src/soup_cli/trainer/mlx_dpo.py +++ b/src/soup_cli/trainer/mlx_dpo.py @@ -27,7 +27,7 @@ class MLXDPOTrainerWrapper: except ImportError as exc: raise ImportError( "MLX backend requires the 'mlx' and 'mlx-lm' packages. " - "Install with: pip install 'soup-cli[mlx]'" + "Install with: pip install \"soup-cli[mlx]\"" ) from exc def setup(self, dataset: dict) -> None: diff --git a/src/soup_cli/trainer/mlx_grpo.py b/src/soup_cli/trainer/mlx_grpo.py index 8939b9d..b4f3d62 100644 --- a/src/soup_cli/trainer/mlx_grpo.py +++ b/src/soup_cli/trainer/mlx_grpo.py @@ -26,7 +26,7 @@ class MLXGRPOTrainerWrapper: except ImportError as exc: raise ImportError( "MLX backend requires the 'mlx' and 'mlx-lm' packages. " - "Install with: pip install 'soup-cli[mlx]'" + "Install with: pip install \"soup-cli[mlx]\"" ) from exc def setup(self, dataset: dict) -> None: diff --git a/src/soup_cli/trainer/mlx_sft.py b/src/soup_cli/trainer/mlx_sft.py index 6d4488c..ae5be08 100644 --- a/src/soup_cli/trainer/mlx_sft.py +++ b/src/soup_cli/trainer/mlx_sft.py @@ -33,7 +33,7 @@ class MLXSFTTrainerWrapper: except ImportError as exc: raise ImportError( "MLX backend requires the 'mlx' and 'mlx-lm' packages. " - "Install with: pip install 'soup-cli[mlx]'" + "Install with: pip install \"soup-cli[mlx]\"" ) from exc def _check_unsupported(self) -> None: diff --git a/src/soup_cli/trainer/online_dpo.py b/src/soup_cli/trainer/online_dpo.py index 593db7b..a4cd681 100644 --- a/src/soup_cli/trainer/online_dpo.py +++ b/src/soup_cli/trainer/online_dpo.py @@ -61,7 +61,7 @@ def _import_online_dpo(): except ImportError as exc: # pragma: no cover — trl ships in [train] raise ImportError( "task='online_dpo' requires trl with OnlineDPO support " - "(pip install 'soup-cli[train]')" + "(pip install \"soup-cli[train]\")" ) from exc diff --git a/src/soup_cli/trainer/sft.py b/src/soup_cli/trainer/sft.py index ccc980a..7a486c1 100644 --- a/src/soup_cli/trainer/sft.py +++ b/src/soup_cli/trainer/sft.py @@ -1098,7 +1098,7 @@ class SFTTrainerWrapper: except ImportError: raise ImportError( "librosa is required for audio training. " - "Install with: pip install 'soup-cli[audio]'" + "Install with: pip install \"soup-cli[audio]\"" ) def load_and_format_audio(example): diff --git a/src/soup_cli/utils/apple_adapter.py b/src/soup_cli/utils/apple_adapter.py index 1c8c265..13e7787 100644 --- a/src/soup_cli/utils/apple_adapter.py +++ b/src/soup_cli/utils/apple_adapter.py @@ -318,7 +318,7 @@ def _load_safetensors_arrays(path: str, field: str) -> dict[str, Any]: raise ImportError( "this adapter holds non-numpy dtypes (likely bf16); " "converting it requires torch " - "(pip install 'soup-cli[train]')" + "(pip install \"soup-cli[train]\")" ) from exc try: tensors = torch_load_file(path) diff --git a/src/soup_cli/utils/compile_tools.py b/src/soup_cli/utils/compile_tools.py index 414fe08..6923ba6 100644 --- a/src/soup_cli/utils/compile_tools.py +++ b/src/soup_cli/utils/compile_tools.py @@ -3,7 +3,7 @@ Generate tool schemas + descriptions optimized via textual gradients. Schema + validators from v0.68.0 Part C; the live optimizer pass lands in v0.71.13 (#227), lazy-importing TextGrad / GEPA with a friendly -``ImportError`` (``pip install 'soup-cli[compile]'``). +``ImportError`` (``pip install "soup-cli[compile]"``). Composes with v0.46 Agent Forge (OpenAPI / MCP / GraphQL parser) — Agent Forge produces the spec, ``compile-tools`` optimises the descriptions. @@ -26,7 +26,7 @@ _SUPPORTED_SPEC_EXTENSIONS: frozenset[str] = frozenset({".json", ".yaml", ".yml" _MAX_OPTIMIZER_NAME_LEN = 32 _INSTALL_HINT = ( - "Run: pip install 'soup-cli[compile]' (installs textgrad / gepa)" + "Run: pip install \"soup-cli[compile]\" (installs textgrad / gepa)" ) # Injectable seam: tests set this to a ``(description, examples, optimizer) diff --git a/src/soup_cli/utils/data_doctor.py b/src/soup_cli/utils/data_doctor.py index 0bf4183..c704f89 100644 --- a/src/soup_cli/utils/data_doctor.py +++ b/src/soup_cli/utils/data_doctor.py @@ -192,7 +192,7 @@ def resolve_tokenizer(tokenizer: Any, *, trust_remote_code: bool = False) -> Any except ImportError as exc: raise ValueError( "soup data doctor needs transformers to load a tokenizer — " - "install with: pip install 'soup-cli[train]'" + "install with: pip install \"soup-cli[train]\"" ) from exc try: return AutoTokenizer.from_pretrained(tokenizer, trust_remote_code=trust_remote_code) diff --git a/src/soup_cli/utils/data_lint.py b/src/soup_cli/utils/data_lint.py index 03b29fb..a73fc06 100644 --- a/src/soup_cli/utils/data_lint.py +++ b/src/soup_cli/utils/data_lint.py @@ -290,7 +290,7 @@ def check_near_duplicates( name="near_duplicates", verdict="OK", message="near-dup check skipped (datasketch not installed)", - evidence="pip install 'soup-cli[data]' to enable", + evidence="pip install \"soup-cli[data]\" to enable", ) if len(rows) < 2: return LintCheck( diff --git a/src/soup_cli/utils/delinearize_llama4.py b/src/soup_cli/utils/delinearize_llama4.py index a2b304e..6701637 100644 --- a/src/soup_cli/utils/delinearize_llama4.py +++ b/src/soup_cli/utils/delinearize_llama4.py @@ -305,7 +305,7 @@ def run_delinearize( except ImportError as exc: raise ImportError( "delinearize-llama4 requires torch + safetensors " - "(pip install 'soup-cli[train]')" + "(pip install \"soup-cli[train]\")" ) from exc # Containment BEFORE makedirs — a directly constructed plan must not diff --git a/src/soup_cli/utils/diagnose/_common.py b/src/soup_cli/utils/diagnose/_common.py index 1adef76..501e905 100644 --- a/src/soup_cli/utils/diagnose/_common.py +++ b/src/soup_cli/utils/diagnose/_common.py @@ -134,7 +134,7 @@ def resolve_tokenizer(tokenizer: object) -> object: except ImportError as exc: raise ValueError( "tokenizer-aware diagnose needs transformers — " - "install with: pip install 'soup-cli[train]'" + "install with: pip install \"soup-cli[train]\"" ) from exc try: return AutoTokenizer.from_pretrained(tokenizer) diff --git a/src/soup_cli/utils/errors.py b/src/soup_cli/utils/errors.py index d4a71d0..ed2bedf 100644 --- a/src/soup_cli/utils/errors.py +++ b/src/soup_cli/utils/errors.py @@ -11,7 +11,7 @@ console = Console(stderr=True) # datasets / bitsandbytes / accelerate) moved out of the core install into the # `[train]` extra. A missing one of these surfaces this single, actionable fix. # The `\\[` escapes the literal `[` for Rich markup (it renders as `[train]`). -_TRAIN_FIX = "Training needs the \\[train] extra. Run: pip install 'soup-cli\\[train]'" +_TRAIN_FIX = "Training needs the \\[train] extra. Run: pip install \"soup-cli\\[train]\"" # Map known error patterns to (short message, fix suggestion) ERROR_MAP = [ @@ -38,22 +38,22 @@ ERROR_MAP = [ ( "No module named 'fastapi'", "FastAPI is not installed (needed for soup serve).", - "Run: pip install 'soup-cli\\[serve]'", + "Run: pip install \"soup-cli\\[serve]\"", ), ( "No module named 'uvicorn'", "Uvicorn is not installed (needed for soup serve).", - "Run: pip install 'soup-cli\\[serve]'", + "Run: pip install \"soup-cli\\[serve]\"", ), ( "No module named 'datasketch'", "Datasketch is not installed (needed for dedup).", - "Run: pip install 'soup-cli\\[data]'", + "Run: pip install \"soup-cli\\[data]\"", ), ( "No module named 'lm_eval'", "lm-evaluation-harness is not installed (needed for eval).", - "Run: pip install 'soup-cli\\[eval]'", + "Run: pip install \"soup-cli\\[eval]\"", ), ( "No module named 'wandb'", @@ -63,12 +63,12 @@ ERROR_MAP = [ ( "No module named 'deepspeed'", "DeepSpeed is not installed.", - "Run: pip install 'soup-cli\\[deepspeed]'", + "Run: pip install \"soup-cli\\[deepspeed]\"", ), ( "No module named 'httpx'", "httpx is not installed (needed for data generate).", - "Run: pip install 'soup-cli\\[generate]'", + "Run: pip install \"soup-cli\\[generate]\"", ), # Heavy training stack — all moved to the [train] extra in v0.71.0. ( diff --git a/src/soup_cli/utils/grpo_long_context.py b/src/soup_cli/utils/grpo_long_context.py index c24cd00..b49afc3 100644 --- a/src/soup_cli/utils/grpo_long_context.py +++ b/src/soup_cli/utils/grpo_long_context.py @@ -150,7 +150,7 @@ def apply_vllm_sleep_mode(engine_args: object) -> object: if version is None: raise RuntimeError( "vllm_sleep_mode requires vLLM >= 0.7 but vLLM is not " - "installed (pip install 'soup-cli[serve-fast]')." + "installed (pip install \"soup-cli[serve-fast]\")." ) if version < _MIN_VLLM_SLEEP_VERSION: found = ".".join(str(part) for part in version) or "unknown" diff --git a/src/soup_cli/utils/liger.py b/src/soup_cli/utils/liger.py index 529a8f3..17ec767 100644 --- a/src/soup_cli/utils/liger.py +++ b/src/soup_cli/utils/liger.py @@ -115,7 +115,7 @@ def validate_liger_config(use_liger: bool, backend: str, device: str) -> list[st if not check_liger_available(): errors.append( "liger-kernel is not installed. " - "Install it with: pip install 'soup-cli[liger]'" + "Install it with: pip install \"soup-cli[liger]\"" ) if backend == "unsloth": diff --git a/src/soup_cli/utils/mii.py b/src/soup_cli/utils/mii.py index 7a011c1..f416f8d 100644 --- a/src/soup_cli/utils/mii.py +++ b/src/soup_cli/utils/mii.py @@ -49,7 +49,7 @@ def create_mii_pipeline( if not is_mii_available(): raise ImportError( "deepspeed-mii is not installed. " - "Install with: pip install 'soup-cli[mii]' " + "Install with: pip install \"soup-cli[mii]\" " "or pip install deepspeed-mii" ) diff --git a/src/soup_cli/utils/prompt_compile.py b/src/soup_cli/utils/prompt_compile.py index 2051cf2..bfeb322 100644 --- a/src/soup_cli/utils/prompt_compile.py +++ b/src/soup_cli/utils/prompt_compile.py @@ -241,7 +241,7 @@ def build_compile_plan( _INSTALL_HINT = ( - "Run: pip install 'soup-cli[compile]' (installs dspy-ai / textgrad / gepa)" + "Run: pip install \"soup-cli[compile]\" (installs dspy-ai / textgrad / gepa)" ) diff --git a/src/soup_cli/utils/prune_prompt.py b/src/soup_cli/utils/prune_prompt.py index 8adfd52..e29b96e 100644 --- a/src/soup_cli/utils/prune_prompt.py +++ b/src/soup_cli/utils/prune_prompt.py @@ -244,7 +244,7 @@ def _resolve_tokenizer(tokenizer: Union[str, Any]) -> Any: except ImportError as exc: raise ValueError( "tokenizer-aware prune-prompt needs transformers — " - "install with: pip install 'soup-cli[train]'" + "install with: pip install \"soup-cli[train]\"" ) from exc try: return AutoTokenizer.from_pretrained(tokenizer) diff --git a/tests/test_v07136.py b/tests/test_v07136.py index b8542d4..e0cb76e 100644 --- a/tests/test_v07136.py +++ b/tests/test_v07136.py @@ -510,8 +510,11 @@ class TestExtrasHintsAreEscaped: def test_plain_exception_hints_are_not_escaped(self): """The counter-rule: non-Rich text must NOT gain a backslash. - `raise ImportError("... pip install 'soup-cli[mlx]'")` never reaches + `raise ImportError('... pip install "soup-cli[mlx]"')` never reaches Rich, so escaping it would surface a literal backslash to the user. + + The quoting itself is v0.71.37's business (cmd.exe cannot strip `'`); + this test only cares that the bracket stays bare here. """ import pathlib @@ -519,7 +522,9 @@ class TestExtrasHintsAreEscaped: root = pathlib.Path(soup_cli.__file__).parent text = (root / "trainer" / "mlx_sft.py").read_text(encoding="utf-8") - assert "'soup-cli[mlx]'" in text + # Quote-agnostic on purpose: the shell quoting around the name is + # v0.71.37's concern, this rule is only about the bracket staying bare. + assert "soup-cli[mlx]" in text assert "soup-cli\\[mlx]" not in text diff --git a/tests/test_v07137.py b/tests/test_v07137.py new file mode 100644 index 0000000..ee3c6f6 --- /dev/null +++ b/tests/test_v07137.py @@ -0,0 +1,139 @@ +"""v0.71.37 — every printed `pip install soup-cli[extra]` hint must be cmd.exe-safe. + +The single-quoted spelling is bash / zsh / PowerShell syntax. Windows +`cmd.exe` has no single-quote quoting: it hands the quotes to pip verbatim and +pip rejects the requirement outright: + + ERROR: Invalid requirement: "'soup-cli[train]'": Expected package name at + the start of dependency specifier + +Measured on Windows before this fix: + + cmd.exe 'soup-cli[train]' -> ERROR "soup-cli[train]" -> ok bare -> ok + PowerShell 'soup-cli[train]' -> ok "soup-cli[train]" -> ok + +Double quotes are the only spelling that works in *every* shell, which is why +the repo already says `pip install -e ".[dev]"`. Bare `soup-cli[train]` is not +an option: zsh globs the bracket and fails with `no matches found`. + +Nothing in Soup can rescue the command once it is typed — pip and the shell own +it, and Soup is not installed yet when the README command runs. The only lever +is what we print, so these tests guard that. +""" + +import pathlib +import re + +import pytest + +# `pip install "soup-cli[x]"` and the Rich-escaped `pip install "soup-cli\[x]"`. +# `\\*` (any run of backslashes), NOT `\\?`: the escaped hint carries TWO +# backslash characters in the source, so `\\?` silently misses every Rich hint — +# including the `soup ui` one that started this — and the guard passes vacuously. +SINGLE_QUOTED = re.compile(r"pip install 'soup-cli\\*\[[a-z][a-z0-9-]*\]'") + + +def _package_root() -> pathlib.Path: + import soup_cli + + return pathlib.Path(soup_cli.__file__).parent + + +def _repo_root() -> pathlib.Path | None: + """src-layout: /src/soup_cli. None when installed as a plain wheel.""" + root = _package_root().parent.parent + return root if (root / "pyproject.toml").is_file() else None + + +class TestHintsAreCmdSafe: + def test_no_single_quoted_hint_in_package(self): + """Nothing under src/soup_cli may print the cmd-hostile spelling.""" + root = _package_root() + offenders = [] + for path in sorted(root.rglob("*.py")): + rel = path.relative_to(root).as_posix() + for lineno, line in enumerate( + path.read_text(encoding="utf-8").splitlines(), start=1 + ): + if SINGLE_QUOTED.search(line): + offenders.append(f"{rel}:{lineno}: {line.strip()}") + assert not offenders, ( + "single-quoted soup-cli[extra] hint — cmd.exe passes the quotes to " + 'pip and it errors out. Use \\"soup-cli[extra]\\" (works in every ' + "shell):\n" + "\n".join(offenders) + ) + + def test_no_single_quoted_hint_in_docs_code_blocks(self): + """No fenced block may hand a reader a command that dies on cmd.exe. + + Scoped to fenced code blocks on purpose: those are what people copy. + Prose is allowed to *name* the broken spelling — the README note does + exactly that, so someone who followed an older video recognises their + error and can search for it. + """ + repo = _repo_root() + if repo is None: + pytest.skip("not an editable/source checkout — docs/ unavailable") + targets = [repo / "README.md", *sorted((repo / "docs").rglob("*.md"))] + offenders = [] + for path in targets: + if not path.is_file(): + continue + # Plans are a historical record of what was decided, not advice. + if "superpowers/plans" in path.as_posix(): + continue + rel = path.relative_to(repo).as_posix() + in_fence = False + for lineno, line in enumerate( + path.read_text(encoding="utf-8").splitlines(), start=1 + ): + if line.lstrip().startswith("```"): + in_fence = not in_fence + continue + if in_fence and SINGLE_QUOTED.search(line): + offenders.append(f"{rel}:{lineno}: {line.strip()}") + assert not offenders, ( + "single-quoted soup-cli[extra] in a docs code block — a Windows " + "cmd.exe reader copies this and gets `Invalid requirement`:\n" + + "\n".join(offenders) + ) + + def test_regex_actually_matches_the_broken_spelling(self): + """Guard the guard: a regex that matches nothing would pass vacuously. + + The first cut used `\\\\?` and matched at most ONE backslash, so it saw + the plain `'soup-cli[fast]'` sites but not a single Rich-escaped one -- + i.e. it would have gone green while `soup ui` still printed a command + that dies on cmd.exe. Both spellings are pinned here on purpose. + """ + # Plain (YAML template comments, plain exception text). + assert SINGLE_QUOTED.search("pip install 'soup-cli[train]'") + # Rich-escaped, exactly as it sits in ui.py: TWO backslash chars. + assert SINGLE_QUOTED.search( + '"Install with: [bold]pip install \'soup-cli\\\\[ui]\'[/]"' + ) + # The spellings we are migrating *to* must not be flagged. + assert not SINGLE_QUOTED.search('pip install "soup-cli[train]"') + assert not SINGLE_QUOTED.search('pip install "soup-cli\\\\[ui]"') + assert not SINGLE_QUOTED.search("pip install soup-cli") + + +class TestDoubleQuotesSurviveRich: + """The bracket still needs escaping — double quotes do not change that.""" + + def test_rich_keeps_escaped_bracket_inside_double_quotes(self): + from io import StringIO + + from rich.console import Console + + def render(markup: str) -> str: + buf = StringIO() + Console(file=buf, force_terminal=False, width=100).print(markup) + return buf.getvalue().strip() + + assert render('[bold]pip install "soup-cli\\[ui]"[/]') == ( + 'pip install "soup-cli[ui]"' + ), "escaped bracket must survive inside double quotes" + assert render('[bold]pip install "soup-cli[ui]"[/]') == ( + 'pip install "soup-cli"' + ), "unescaped bracket is still eaten — quoting does not fix escaping"