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
3 changes: 2 additions & 1 deletion src/dspy_cli/server/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()})")
Comment thread
isaacbmiller marked this conversation as resolved.
click.echo(" - Make sure you have dspy-cli as a local dependency")
click.echo(" - Install them using pip install -e .")

Expand Down
20 changes: 19 additions & 1 deletion src/dspy_cli/utils/venv.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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:")
Expand Down
33 changes: 32 additions & 1 deletion tests/test_venv_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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"