diff --git a/crates/uv/src/commands/project/add.rs b/crates/uv/src/commands/project/add.rs index 6a6f6753daec..4f0d501df74c 100644 --- a/crates/uv/src/commands/project/add.rs +++ b/crates/uv/src/commands/project/add.rs @@ -44,7 +44,8 @@ use crate::commands::pip::loggers::{ use crate::commands::pip::operations::Modifications; use crate::commands::project::lock::LockMode; use crate::commands::project::{ - init_script_python_requirement, validate_script_requires_python, ProjectError, ScriptPython, + init_script_python_requirement, validate_script_requires_python, ProjectError, + ProjectInterpreter, ScriptPython, }; use crate::commands::reporters::{PythonDownloadReporter, ResolverReporter}; use crate::commands::{diagnostics, pip, project, ExitStatus, SharedState}; @@ -214,22 +215,43 @@ pub(crate) async fn add( } } - // Discover or create the virtual environment. - let venv = project::get_or_init_environment( - project.workspace(), - python.as_deref().map(PythonRequest::parse), - python_preference, - python_downloads, - connectivity, - native_tls, - allow_insecure_host, - no_config, - cache, - printer, - ) - .await?; + if frozen || no_sync { + // Discover the interpreter. + let interpreter = ProjectInterpreter::discover( + project.workspace(), + project_dir, + python.as_deref().map(PythonRequest::parse), + python_preference, + python_downloads, + connectivity, + native_tls, + allow_insecure_host, + no_config, + cache, + printer, + ) + .await? + .into_interpreter(); + + Target::Project(project, Box::new(PythonTarget::Interpreter(interpreter))) + } else { + // Discover or create the virtual environment. + let venv = project::get_or_init_environment( + project.workspace(), + python.as_deref().map(PythonRequest::parse), + python_preference, + python_downloads, + connectivity, + native_tls, + allow_insecure_host, + no_config, + cache, + printer, + ) + .await?; - Target::Project(project, venv) + Target::Project(project, Box::new(PythonTarget::Environment(venv))) + } }; let client_builder = BaseClientBuilder::new() @@ -576,8 +598,8 @@ pub(crate) async fn add( } }; - let (project, venv) = match target { - Target::Project(project, venv) => (project, venv), + let (project, environment) = match target { + Target::Project(project, environment) => (project, environment), // If `--script`, exit early. There's no reason to lock and sync. Target::Script(script, _) => { writeln!( @@ -627,10 +649,9 @@ pub(crate) async fn add( project, &mut toml, &edits, - &venv, + &environment, state, locked, - no_sync, &dependency_type, raw_sources, settings.as_ref(), @@ -688,10 +709,9 @@ async fn lock_and_sync( mut project: VirtualProject, toml: &mut PyProjectTomlMut, edits: &[DependencyEdit], - venv: &PythonEnvironment, + environment: &PythonTarget, state: SharedState, locked: bool, - no_sync: bool, dependency_type: &DependencyType, raw_sources: bool, settings: ResolverInstallerSettingsRef<'_>, @@ -704,9 +724,9 @@ async fn lock_and_sync( printer: Printer, ) -> Result<(), ProjectError> { let mode = if locked { - LockMode::Locked(venv.interpreter()) + LockMode::Locked(environment.interpreter()) } else { - LockMode::Write(venv.interpreter()) + LockMode::Write(environment.interpreter()) }; let mut lock = project::lock::do_safe_lock( @@ -846,9 +866,10 @@ async fn lock_and_sync( } } - if no_sync { + let PythonTarget::Environment(venv) = environment else { + // If we're not syncing, exit early. return Ok(()); - } + }; // Sync the environment. let (extras, dev) = match dependency_type { @@ -1024,8 +1045,9 @@ fn resolve_requirement( enum Target { /// A PEP 723 script, with inline metadata. Script(Pep723Script, Box), + /// A project with a `pyproject.toml`. - Project(VirtualProject, PythonEnvironment), + Project(VirtualProject, Box), } impl Target { @@ -1038,6 +1060,24 @@ impl Target { } } +/// A Python [`Interpreter`] or [`PythonEnvironment`] for a project. +#[derive(Debug)] +#[allow(clippy::large_enum_variant)] +enum PythonTarget { + Interpreter(Interpreter), + Environment(PythonEnvironment), +} + +impl PythonTarget { + /// Return the [`Interpreter`] for the project. + fn interpreter(&self) -> &Interpreter { + match self { + Self::Interpreter(interpreter) => interpreter, + Self::Environment(venv) => venv.interpreter(), + } + } +} + #[derive(Debug, Clone)] struct DependencyEdit { dependency_type: DependencyType, diff --git a/crates/uv/tests/it/edit.rs b/crates/uv/tests/it/edit.rs index c93bfec3fbc2..04f25d4ad4f1 100644 --- a/crates/uv/tests/it/edit.rs +++ b/crates/uv/tests/it/edit.rs @@ -3675,6 +3675,9 @@ fn add_puts_default_indentation_in_pyproject_toml_if_not_observed() -> Result<() fn add_frozen() -> Result<()> { let context = TestContext::new("3.12"); + // Remove the virtual environment. + fs_err::remove_dir_all(&context.venv)?; + let pyproject_toml = context.temp_dir.child("pyproject.toml"); pyproject_toml.write_str(indoc! {r#" [project] @@ -3694,6 +3697,7 @@ fn add_frozen() -> Result<()> { ----- stdout ----- ----- stderr ----- + Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] "###); let pyproject_toml = context.read("pyproject.toml"); @@ -3719,6 +3723,7 @@ fn add_frozen() -> Result<()> { }); assert!(!context.temp_dir.join("uv.lock").exists()); + assert!(!context.venv.exists()); Ok(()) } @@ -3728,6 +3733,9 @@ fn add_frozen() -> Result<()> { fn add_no_sync() -> Result<()> { let context = TestContext::new("3.12"); + // Remove the virtual environment. + fs_err::remove_dir_all(&context.venv)?; + let pyproject_toml = context.temp_dir.child("pyproject.toml"); pyproject_toml.write_str(indoc! {r#" [project] @@ -3747,6 +3755,7 @@ fn add_no_sync() -> Result<()> { ----- stdout ----- ----- stderr ----- + Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] Resolved 4 packages in [TIME] "###); @@ -3773,6 +3782,7 @@ fn add_no_sync() -> Result<()> { }); assert!(context.temp_dir.join("uv.lock").exists()); + assert!(!context.venv.exists()); Ok(()) }