diff --git a/src/soup_cli/commands/data.py b/src/soup_cli/commands/data.py index 3df4489..92703dc 100644 --- a/src/soup_cli/commands/data.py +++ b/src/soup_cli/commands/data.py @@ -301,7 +301,7 @@ def dedup( semantic: bool = typer.Option( False, "--semantic", help="Use embedding cosine (SemDeDup) instead of MinHash. Catches " - "paraphrases MinHash misses. Requires soup-cli[train].", + "paraphrases MinHash misses. Requires soup-cli\\[train].", ), embed_model: str = typer.Option( DEFAULT_EMBED_MODEL, "--embed-model", diff --git a/src/soup_cli/commands/eval.py b/src/soup_cli/commands/eval.py index f2410b3..c0ccbbe 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 96f61be..a43c88b 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 d361ea5..46c28da 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/merge_sharded_fsdp_weights.py b/src/soup_cli/commands/merge_sharded_fsdp_weights.py index 0255c25..5906a91 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 bb9d725..a9b3930 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 d9a2121..b96bea8 100644 --- a/src/soup_cli/commands/train.py +++ b/src/soup_cli/commands/train.py @@ -377,7 +377,7 @@ def train( "--track-energy", help=( "Measure the training window's energy + CO2 via codecarbon " - "(offline; requires `pip install soup-cli[carbon]`). Feeds the " + "(offline; requires `pip install soup-cli\\[carbon]`). Feeds the " "kWh / CO2 into --annex-xi. v0.71.3." ), ), @@ -711,7 +711,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: @@ -1484,7 +1484,7 @@ def train( else: console.print( "[yellow]--track-energy:[/] no reading " - "(install `pip install soup-cli[carbon]`)" + "(install `pip install soup-cli\\[carbon]`)" ) # --- v0.71.15 #244 --energy-out: persist for `soup bom emit --energy` - @@ -1504,7 +1504,7 @@ def train( else: console.print( "[yellow]--energy-out skipped:[/] no energy reading " - "(set --track-energy + install `pip install soup-cli[carbon]`)" + "(set --track-energy + install `pip install soup-cli\\[carbon]`)" ) # --- v0.59.0 --annex-xi: Annex XI/XII auto-doc ----------------------- diff --git a/src/soup_cli/commands/tui.py b/src/soup_cli/commands/tui.py index 9b2a509..1c9c66b 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 1af3467..69a90b1 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/tests/test_v07136.py b/tests/test_v07136.py index dbc1782..d9e10fb 100644 --- a/tests/test_v07136.py +++ b/tests/test_v07136.py @@ -378,6 +378,107 @@ class TestGreedySemdedup: rep.threshold = 0.5 +class TestExtrasHintsAreEscaped: + """Rich eats `[extra]` as a markup tag unless it is escaped. + + Unescaped, `pip install 'soup-cli[eval]'` renders as + `pip install 'soup-cli'` -- which installs the base package WITHOUT the + extra the user is missing, so following the hint appears to succeed and + the feature still fails. Found during v0.71.36; same class as the + v0.71.28 \\[mcp] fix, which only fixed its own site. + + Sites that do NOT go through Rich (plain exception text, docstrings) + must NOT be escaped -- a backslash would show up literally. + """ + + # Rich style tags. A hint string carrying one of these is console.print + # output; a bare `raise ImportError("... soup-cli[mlx]")` is NOT and must + # be left alone (escaping it would print a literal backslash). + _RICH_TAGS = ("[/]", "[bold]", "[red]", "[yellow]", "[green]", "[dim]") + + def test_rich_renders_unescaped_bracket_away(self): + """Pin the underlying Rich behaviour this whole class exists for.""" + from io import StringIO + + from rich.console import Console + + def render(markup): + buf = StringIO() + Console(file=buf, force_terminal=False, width=100).print(markup) + return buf.getvalue().strip() + + assert render("[bold]pip install 'soup-cli[train]'[/]") == ( + "pip install 'soup-cli'" + ), "unescaped bracket must be eaten (this is the bug)" + assert render("[bold]pip install 'soup-cli\\[train]'[/]") == ( + "pip install 'soup-cli[train]'" + ), "escaped bracket must survive (this is the fix)" + + def test_no_unescaped_extras_hint_in_rich_markup(self): + """Every hint carrying Rich markup must escape its bracket.""" + import pathlib + import re + + import soup_cli + + root = pathlib.Path(soup_cli.__file__).parent + bad_re = re.compile(r"(?