Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 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
6 changes: 5 additions & 1 deletion src/pyvm_updater/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -560,23 +560,27 @@
Examples:
pyvm venv create myproject
pyvm venv create myproject --python 3.12
pyvm venv create myproject --path ./venv
pyvm venv create myproject --requirements requirements.txt
"""
from pathlib import Path as PathLib

from .venv import create_venv

venv_path = PathLib(path) if path else None
req_path = PathLib(requirements) if requirements else None

Check failure on line 570 in src/pyvm_updater/cli.py

View workflow job for this annotation

GitHub Actions / Lint (Ruff)

Ruff (F821)

src/pyvm_updater/cli.py:570:41: F821 Undefined name `requirements`

Check failure on line 570 in src/pyvm_updater/cli.py

View workflow job for this annotation

GitHub Actions / Lint (Ruff)

Ruff (F821)

src/pyvm_updater/cli.py:570:24: F821 Undefined name `requirements`

click.echo(f"Creating venv '{name}'...")
if python_version:
click.echo(f"Using Python {python_version}")
if req_path:
click.echo(f"Installing dependencies from {req_path.name}")

success, message = create_venv(
name=name,
python_version=python_version,
path=venv_path,
system_site_packages=system_site_packages,
requirements_file=req_path,
)

if success:
Expand Down
32 changes: 31 additions & 1 deletion src/pyvm_updater/installers.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,37 @@

def update_python_windows(version_str: str, preferred: str = "auto") -> bool:
"""Update Python on Windows."""
return _install_with_plugins(version_str, preferred=preferred)
print("\n🪟 Windows detected - Downloading Python installer...")

if not validate_version_string(version_str):

Check failure on line 15 in src/pyvm_updater/installers.py

View workflow job for this annotation

GitHub Actions / Lint (Ruff)

Ruff (F821)

src/pyvm_updater/installers.py:15:12: F821 Undefined name `validate_version_string`
print(f"Error: Invalid version string: {version_str}")
return False

try:
parts = version_str.split(".")
if len(parts) < 3:
print(f"Error: Version must be major.minor.patch format: {version_str}")
return False
major, minor = parts[0], parts[1]
except (ValueError, IndexError) as e:
print(f"Error parsing version: {e}")
return False

machine = platform.machine().lower()

Check failure on line 29 in src/pyvm_updater/installers.py

View workflow job for this annotation

GitHub Actions / Lint (Ruff)

Ruff (F821)

src/pyvm_updater/installers.py:29:15: F821 Undefined name `platform`
if machine in ["amd64", "x86_64"]:
arch = "amd64"
elif machine in ["arm64", "aarch64"]:
try:
major_int, minor_int = int(major), int(minor)
if major_int < 3 or (major_int == 3 and minor_int < 11):
print("ARM64 installers are only available for Python 3.11+")
arch = "amd64"
else:
arch = "arm64"
except (ValueError, TypeError):
arch = "amd64"
else:
arch = "win32"

Check failure on line 43 in src/pyvm_updater/installers.py

View workflow job for this annotation

GitHub Actions / Lint (Ruff)

Ruff (F841)

src/pyvm_updater/installers.py:43:9: F841 Local variable `arch` is assigned to but never used


def update_python_linux(version_str: str, build_from_source: bool = False, preferred: str = "auto") -> bool:
Expand Down
3 changes: 2 additions & 1 deletion src/pyvm_updater/tui.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,8 @@ def compose(self) -> ComposeResult:
yield Label("Loading...")

yield Static(
"[dim]Tab: switch panels | Arrow keys: navigate | Enter: install | X: remove | R: refresh | U: update | B: rollback | Q: quit[/dim]",
"[dim]Tab: switch panels | Arrow keys: navigate | Enter: install | X: remove | "
"R: refresh | U: update | B: rollback | Q: quit[/dim]",
id="hint-bar",
)

Expand Down
41 changes: 39 additions & 2 deletions src/pyvm_updater/venv.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ def create_venv(
python_version: str | None = None,
path: Path | None = None,
system_site_packages: bool = False,
requirements_file: Path | None = None,
) -> tuple[bool, str]:
"""Create a new virtual environment.

Expand All @@ -120,6 +121,7 @@ def create_venv(
python_version: Python version to use (e.g., "3.12"). If None, uses current Python.
path: Custom path for venv. If None, uses default location.
system_site_packages: Whether to include system site-packages.
requirements_file: Path to requirements.txt file to install.

Returns:
Tuple of (success, message).
Expand All @@ -146,7 +148,6 @@ def create_venv(
python_exe = sys.executable
python_version = f"{sys.version_info.major}.{sys.version_info.minor}"

# Build venv command
cmd = [python_exe, "-m", "venv"]
if system_site_packages:
cmd.append("--system-site-packages")
Expand All @@ -169,7 +170,43 @@ def create_venv(
}
save_venv_registry(registry)

return True, f"Created venv '{name}' at {venv_path}"
success_msg = f"Created venv '{name}' at {venv_path}"

# Install requirements if specified
if requirements_file:
os_name, _ = get_os_info()
if os_name == "windows":
pip_exe = venv_path / "Scripts" / "pip.exe"
else:
pip_exe = venv_path / "bin" / "pip"

if not pip_exe.exists():
if os_name == "windows":
python_in_venv = venv_path / "Scripts" / "python.exe"
else:
python_in_venv = venv_path / "bin" / "python"
pip_cmd = [str(python_in_venv), "-m", "pip"]
else:
pip_cmd = [str(pip_exe)]

try:
# Upgrade pip first (optional helper)
# subprocess.run(pip_cmd + ["install", "--upgrade", "pip"], capture_output=True, check=False)

# Install requirements
subprocess.run(
pip_cmd + ["install", "-r", str(requirements_file)], capture_output=True, text=True, check=True
)
success_msg += f"\n Installed requirements from {requirements_file.name}"
except subprocess.CalledProcessError as e:
error_output = e.stderr or e.stdout
return (
True,
f"{success_msg}\n ⚠️ Warning: Failed to install requirements from {requirements_file.name}:\n"
f"{error_output}",
)

return True, success_msg

except subprocess.CalledProcessError as e:
return (
Expand Down
23 changes: 23 additions & 0 deletions tests/test_venv.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,29 @@ def test_create_venv_already_exists(self, temp_venv_dir):
assert success is False
assert "already exists" in message

def test_create_venv_with_requirements(self, temp_venv_dir):
"""Test venv creation with requirements file."""
venv_path = temp_venv_dir / "req_venv"
req_file = temp_venv_dir / "requirements.txt"
req_file.write_text("requests==2.25.0")

with patch("pyvm_updater.venv.get_venv_dir", return_value=temp_venv_dir):
with patch("pyvm_updater.venv.save_venv_registry"):
with patch("pyvm_updater.venv.subprocess.run") as mock_run:
success, message = create_venv("req_venv", path=venv_path, requirements_file=req_file)

assert success is True
assert "Installed requirements" in message

# Verify pip install was called
assert mock_run.call_count == 2

args, _ = mock_run.call_args_list[1]
cmd = args[0]
assert "install" in cmd
assert "-r" in cmd
assert str(req_file) in cmd


class TestListVenvs:
"""Tests for list_venvs function."""
Expand Down
Loading