From 5010f66d50c9c2c9285ed85cfb481aea7fd83861 Mon Sep 17 00:00:00 2001 From: isaacbmiller Date: Sat, 28 Feb 2026 14:59:46 -0500 Subject: [PATCH] feat: add fish shell support for venv activation instructions Detect the user's shell (fish, zsh, bash) and show the correct venv activation command. Fish shell users now see 'source .venv/bin/activate.fish' instead of the bash-only 'source .venv/bin/activate'. Closes #36 Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- src/dspy_cli/server/runner.py | 3 ++- src/dspy_cli/utils/venv.py | 20 +++++++++++++++++++- tests/test_venv_utils.py | 33 ++++++++++++++++++++++++++++++++- 3 files changed, 53 insertions(+), 3 deletions(-) diff --git a/src/dspy_cli/server/runner.py b/src/dspy_cli/server/runner.py index b1636c3..a808d80 100644 --- a/src/dspy_cli/server/runner.py +++ b/src/dspy_cli/server/runner.py @@ -221,7 +221,8 @@ def notify_cli(msg: str, level: str = "info"): click.echo(" 2. Subclass dspy.Module") click.echo(" 3. Are not named with a leading underscore") click.echo(" 4. If you are using external dependencies:") - click.echo(" - Ensure your venv is activated") + from dspy_cli.utils.venv import venv_activate_command + click.echo(f" - Ensure your venv is activated ({venv_activate_command()})") click.echo(" - Make sure you have dspy-cli as a local dependency") click.echo(" - Install them using pip install -e .") diff --git a/src/dspy_cli/utils/venv.py b/src/dspy_cli/utils/venv.py index d302cd3..125459d 100644 --- a/src/dspy_cli/utils/venv.py +++ b/src/dspy_cli/utils/venv.py @@ -143,8 +143,26 @@ def validate_python_version(python: Path, min_version: tuple = (3, 9)) -> tuple[ return False, "" +def _is_fish_shell() -> bool: + """Return True when the current interactive shell is fish.""" + if os.environ.get("FISH_VERSION"): + return True + return os.environ.get("SHELL", "").endswith("/fish") + + +def venv_activate_command(venv_dir: str = ".venv") -> str: + """Return the shell-appropriate venv activation command.""" + if sys.platform == "win32": + return f"{venv_dir}\\Scripts\\activate" + if _is_fish_shell(): + return f"source {venv_dir}/bin/activate.fish" + return f"source {venv_dir}/bin/activate" + + def show_venv_warning(): """Display warning and guidance when no venv is detected.""" + activate_cmd = venv_activate_command() + click.echo(click.style("⚠ Warning: No virtual environment detected", fg="yellow")) click.echo() click.echo("Running without a project venv may cause import errors if dependencies") @@ -154,7 +172,7 @@ def show_venv_warning(): click.echo(" 1. Add dspy-cli to your project:") click.echo(" uv add dspy-cli") click.echo(" 2. Create and activate a venv:") - click.echo(" uv sync (or python -m venv .venv && source .venv/bin/activate)") + click.echo(f" uv sync (or python -m venv .venv && {activate_cmd})") click.echo(" 3. Use a task runner:") click.echo(" uv run dspy-cli serve") click.echo(" 4. Specify Python interpreter:") diff --git a/tests/test_venv_utils.py b/tests/test_venv_utils.py index 4a0efc5..8886298 100644 --- a/tests/test_venv_utils.py +++ b/tests/test_venv_utils.py @@ -7,7 +7,12 @@ from pathlib import Path -from dspy_cli.utils.venv import sanitize_env_for_exec, validate_python_version +from dspy_cli.utils.venv import ( + _is_fish_shell, + sanitize_env_for_exec, + validate_python_version, + venv_activate_command, +) class TestValidatePythonVersion: @@ -119,3 +124,29 @@ def test_preserves_other_vars(self): # Cleanup os.environ.pop("MY_CUSTOM_VAR", None) + + +class TestVenvActivateCommand: + """Tests for shell-aware activation command.""" + + def test_fish_via_env_var(self, monkeypatch): + monkeypatch.setattr(sys, "platform", "darwin") + monkeypatch.setenv("FISH_VERSION", "3.6.1") + assert _is_fish_shell() + assert venv_activate_command() == "source .venv/bin/activate.fish" + + def test_fish_via_shell_path(self, monkeypatch): + monkeypatch.setattr(sys, "platform", "darwin") + monkeypatch.delenv("FISH_VERSION", raising=False) + monkeypatch.setenv("SHELL", "/usr/local/bin/fish") + assert _is_fish_shell() + + def test_non_fish_unix(self, monkeypatch): + monkeypatch.setattr(sys, "platform", "darwin") + monkeypatch.delenv("FISH_VERSION", raising=False) + monkeypatch.setenv("SHELL", "/bin/bash") + assert venv_activate_command() == "source .venv/bin/activate" + + def test_windows(self, monkeypatch): + monkeypatch.setattr(sys, "platform", "win32") + assert venv_activate_command() == r".venv\Scripts\activate"