diff --git a/notes/pep508.md b/notes/pep508.md index f2cf2b1ede..0238c8efb8 100644 --- a/notes/pep508.md +++ b/notes/pep508.md @@ -96,7 +96,7 @@ dependencies = [ ] [project.dependencies_meta.0] -exta_information = 42 +extra_information = 42 ``` Unfortunately that would still cause issues for tools that locally interpret `pyproject.toml` diff --git a/rye/src/cli/list.rs b/rye/src/cli/list.rs index bd7c2c46e4..92812e25a3 100644 --- a/rye/src/cli/list.rs +++ b/rye/src/cli/list.rs @@ -3,10 +3,9 @@ use std::path::PathBuf; use anyhow::Error; use clap::Parser; -use crate::bootstrap::ensure_self_venv; use crate::pyproject::PyProject; use crate::utils::{get_venv_python_bin, CommandOutput}; -use crate::uv::{UvBuilder, UvWithVenv}; +use crate::uv::{UvBuilder, Venv}; /// Prints the currently installed packages. #[derive(Parser, Debug)] @@ -20,23 +19,12 @@ pub fn execute(cmd: Args) -> Result<(), Error> { let project = PyProject::load_or_discover(cmd.pyproject.as_deref())?; let python = get_venv_python_bin(&project.venv_path()); if !python.is_file() { + warn!("Project is not synced, no virtualenv found. Run `rye sync`."); return Ok(()); } - let _ = ensure_self_venv(CommandOutput::Normal)?; - let uv = UvBuilder::new() .with_output(CommandOutput::Normal) .ensure_exists()?; - if !project.rye_managed() { - UvWithVenv::new(uv, &project.venv_path(), &project.venv_python_version()?).freeze()?; - } else { - uv.venv( - &project.venv_path(), - &python, - &project.venv_python_version()?, - None, - )? - .freeze()?; - } + uv.read_only_venv(&project.venv_path())?.freeze()?; Ok(()) } diff --git a/rye/src/uv.rs b/rye/src/uv.rs index d20d0370af..e1d629df60 100644 --- a/rye/src/uv.rs +++ b/rye/src/uv.rs @@ -295,23 +295,38 @@ impl Uv { cmd } - /// Ensures a venv is exists or is created at the given path. - /// Returns a UvWithVenv that can be used to run commands in the venv. + /// Ensures a venv exists, creating it at the given path if necessary. + /// + /// Returns a [`ReadWriteVenv`] that can be used to run commands in the venv. pub fn venv( &self, venv_dir: &Path, py_bin: &Path, version: &PythonVersion, prompt: Option<&str>, - ) -> Result { + ) -> Result { match read_venv_marker(venv_dir) { Some(venv) if venv.is_compatible(version) => { - Ok(UvWithVenv::new(self.clone(), venv_dir, version)) + Ok(ReadWriteVenv::new(self.clone(), venv_dir, version)) } _ => self.create_venv(venv_dir, py_bin, version, prompt), } } + /// Returns a [`ReadOnlyVenv`] that can be used to run commands in the venv. + /// + /// Returns an error if the venv does not exist. + pub fn read_only_venv(&self, venv_dir: &Path) -> Result { + if venv_dir.is_dir() { + Ok(ReadOnlyVenv::new(self.clone(), venv_dir)) + } else { + Err(anyhow!( + "Virtualenv not found at path: {}", + venv_dir.display() + )) + } + } + /// Get uv binary path /// /// Warning: Always use self.cmd() when at all possible @@ -325,7 +340,7 @@ impl Uv { py_bin: &Path, version: &PythonVersion, prompt: Option<&str>, - ) -> Result { + ) -> Result { let mut cmd = self.cmd(); cmd.arg("venv").arg("--python").arg(py_bin); if let Some(prompt) = prompt { @@ -348,7 +363,7 @@ impl Uv { status )); } - Ok(UvWithVenv::new(self.clone(), venv_dir, version)) + Ok(ReadWriteVenv::new(self.clone(), venv_dir, version)) } #[allow(clippy::too_many_arguments)] @@ -406,31 +421,92 @@ impl Uv { } } -// Represents a venv generated and managed by uv -pub struct UvWithVenv { +/// Represents uv with any venv +/// +/// Methods on this type are not allowed to create or modify the underlying virtualenv +pub struct ReadOnlyVenv { + uv: Uv, + venv_path: PathBuf, +} + +/// Represents a venv generated and managed by uv +pub struct ReadWriteVenv { uv: Uv, venv_path: PathBuf, py_version: PythonVersion, } -impl UvWithVenv { - pub fn new(uv: Uv, venv_dir: &Path, version: &PythonVersion) -> Self { - UvWithVenv { - uv, - py_version: version.clone(), - venv_path: venv_dir.to_path_buf(), - } - } +pub trait Venv { + fn cmd(&self) -> Command; + + fn venv_path(&self) -> &Path; /// Returns a new command with the uv binary as the command to run. /// The command will have the correct proxy settings and verbosity level based on CommandOutput. /// The command will also have the VIRTUAL_ENV environment variable set to the venv path. fn venv_cmd(&self) -> Command { - let mut cmd = self.uv.cmd(); - cmd.env("VIRTUAL_ENV", &self.venv_path); + let mut cmd = self.cmd(); + cmd.env("VIRTUAL_ENV", self.venv_path()); cmd } + /// Freezes the venv. + fn freeze(&self) -> Result<(), Error> { + let status = self + .venv_cmd() + .arg("pip") + .arg("freeze") + .status() + .with_context(|| format!("unable to freeze venv at {}", self.venv_path().display()))?; + + if !status.success() { + return Err(anyhow!( + "Failed to freeze venv at {}. uv exited with status: {}", + self.venv_path().display(), + status + )); + } + + Ok(()) + } +} + +impl Venv for ReadOnlyVenv { + fn cmd(&self) -> Command { + self.uv.cmd() + } + fn venv_path(&self) -> &Path { + &self.venv_path + } +} + +impl Venv for ReadWriteVenv { + fn cmd(&self) -> Command { + self.uv.cmd() + } + fn venv_path(&self) -> &Path { + &self.venv_path + } +} + +impl ReadOnlyVenv { + pub fn new(uv: Uv, venv_dir: &Path) -> Self { + Self { + uv, + venv_path: venv_dir.to_path_buf(), + } + } +} + +impl ReadWriteVenv { + pub fn new(uv: Uv, venv_dir: &Path, version: &PythonVersion) -> Self { + ReadWriteVenv { + uv, + py_version: version.clone(), + venv_path: venv_dir.to_path_buf(), + } + } + /// Writes a rye-venv.json for this venv. pub fn write_marker(&self) -> Result<(), Error> { write_venv_marker(&self.venv_path, &self.py_version) @@ -438,7 +514,7 @@ impl UvWithVenv { /// Set the output level for subsequent invocations of uv. pub fn with_output(self, output: CommandOutput) -> Self { - UvWithVenv { + Self { uv: Uv { output, ..self.uv }, venv_path: self.venv_path, py_version: self.py_version, @@ -473,26 +549,6 @@ impl UvWithVenv { Ok(()) } - /// Freezes the venv. - pub fn freeze(&self) -> Result<(), Error> { - let status = self - .venv_cmd() - .arg("pip") - .arg("freeze") - .status() - .with_context(|| format!("unable to freeze venv at {}", self.venv_path.display()))?; - - if !status.success() { - return Err(anyhow!( - "Failed to freeze venv at {}. uv exited with status: {}", - self.venv_path.display(), - status - )); - } - - Ok(()) - } - /// Installs the given requirement in the venv. /// /// If you provide a list of extras, they will be installed as well. diff --git a/rye/tests/test-list.rs b/rye/tests/test-list.rs index 1c7a55fadb..391c1a42ce 100644 --- a/rye/tests/test-list.rs +++ b/rye/tests/test-list.rs @@ -56,3 +56,45 @@ fn test_list_not_rye_managed() { ----- stderr ----- "###); } + +#[test] +fn test_list_never_overwrite() { + let space = Space::new(); + space.init("my-project"); + + space.rye_cmd().arg("sync").status().expect("Sync failed"); + + let venv_marker = space.read_string(".venv/rye-venv.json"); + assert!( + venv_marker.contains("@3.12"), + "asserting contents of venv marker: {}", + venv_marker + ); + + // Pick different python version + space + .rye_cmd() + .arg("pin") + .arg("3.11") + .status() + .expect("Sync failed"); + + // List keeps the existing virtualenv unchanged + + rye_cmd_snapshot!( + space.rye_cmd().arg("list"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + -e file:[TEMP_PATH]/project + + ----- stderr ----- + "###); + + let venv_marker = space.read_string(".venv/rye-venv.json"); + assert!( + venv_marker.contains("@3.12"), + "asserting contents of venv marker: {}", + venv_marker + ); +}