diff --git a/crates/uv/src/commands/project/init.rs b/crates/uv/src/commands/project/init.rs index 3560ad54b117..6685ca67032e 100644 --- a/crates/uv/src/commands/project/init.rs +++ b/crates/uv/src/commands/project/init.rs @@ -17,8 +17,9 @@ use uv_git::GIT; use uv_pep440::Version; use uv_pep508::PackageName; use uv_python::{ - EnvironmentPreference, PythonDownloads, PythonInstallation, PythonPreference, PythonRequest, - PythonVariant, PythonVersionFile, VersionFileDiscoveryOptions, VersionRequest, + EnvironmentPreference, PythonDownloads, PythonEnvironment, PythonInstallation, + PythonPreference, PythonRequest, PythonVariant, PythonVersionFile, VersionFileDiscoveryOptions, + VersionRequest, }; use uv_resolver::RequiresPython; use uv_scripts::{Pep723Script, ScriptTag}; @@ -409,7 +410,7 @@ async fn init_project( } else { let interpreter = PythonInstallation::find_or_download( Some(python_request), - EnvironmentPreference::Any, + EnvironmentPreference::OnlySystem, python_preference, python_downloads, &client_builder, @@ -431,7 +432,7 @@ async fn init_project( python_request => { let interpreter = PythonInstallation::find_or_download( Some(&python_request), - EnvironmentPreference::Any, + EnvironmentPreference::OnlySystem, python_preference, python_downloads, &client_builder, @@ -457,8 +458,29 @@ async fn init_project( (requires_python, python_request) } } + } else if let Ok(virtualenv) = PythonEnvironment::from_root(path.join(".venv"), cache) { + // (2) An existing Python environment in the target directory + debug!("Using Python version from existing virtual environment in project"); + let interpreter = virtualenv.into_interpreter(); + + let requires_python = + RequiresPython::greater_than_equal_version(&interpreter.python_minor_version()); + + // Pin to the minor version. + let python_request = if no_pin_python { + None + } else { + Some(PythonRequest::Version(VersionRequest::MajorMinor( + interpreter.python_major(), + interpreter.python_minor(), + PythonVariant::Default, + ))) + }; + + (requires_python, python_request) } else if let Some(requires_python) = workspace.as_ref().and_then(find_requires_python) { - // (2) `requires-python` from the workspace + // (3) `requires-python` from the workspace + debug!("Using Python version from project workspace"); let python_request = PythonRequest::Version(VersionRequest::Range( requires_python.specifiers().clone(), PythonVariant::Default, @@ -470,7 +492,7 @@ async fn init_project( } else { let interpreter = PythonInstallation::find_or_download( Some(&python_request), - EnvironmentPreference::Any, + EnvironmentPreference::OnlySystem, python_preference, python_downloads, &client_builder, @@ -489,10 +511,10 @@ async fn init_project( (requires_python, python_request) } else { - // (3) Default to the system Python + // (4) Default to the system Python let interpreter = PythonInstallation::find_or_download( None, - EnvironmentPreference::Any, + EnvironmentPreference::OnlySystem, python_preference, python_downloads, &client_builder, diff --git a/crates/uv/tests/it/init.rs b/crates/uv/tests/it/init.rs index a40ac347c5dc..cf4f40970428 100644 --- a/crates/uv/tests/it/init.rs +++ b/crates/uv/tests/it/init.rs @@ -2042,6 +2042,103 @@ fn init_requires_python_version_file() -> Result<()> { Ok(()) } +/// Run `uv init`, inferring the Python version from an existing `.venv` +#[test] +fn init_existing_environment() -> Result<()> { + let context = TestContext::new_with_versions(&["3.8", "3.12"]); + + let child = context.temp_dir.child("foo"); + child.create_dir_all()?; + + // Create a new virtual environment in the directory + uv_snapshot!(context.filters(), context.venv().current_dir(&child).arg("--python").arg("3.12"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] + Creating virtual environment at: .venv + Activate with: source .venv/[BIN]/activate + "###); + + uv_snapshot!(context.filters(), context.init().current_dir(&context.temp_dir).arg(child.as_os_str()), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Initialized project `foo` at `[TEMP_DIR]/foo` + "###); + + let pyproject_toml = fs_err::read_to_string(child.join("pyproject.toml"))?; + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + pyproject_toml, @r###" + [project] + name = "foo" + version = "0.1.0" + description = "Add your description here" + readme = "README.md" + requires-python = ">=3.12" + dependencies = [] + "### + ); + }); + + Ok(()) +} + +/// Run `uv init`, it should ignore a the Python version from a parent `.venv` +#[test] +fn init_existing_environment_parent() -> Result<()> { + let context = TestContext::new_with_versions(&["3.8", "3.12"]); + + // Create a new virtual environment in the parent directory + uv_snapshot!(context.filters(), context.venv().current_dir(&context.temp_dir).arg("--python").arg("3.12"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] + Creating virtual environment at: .venv + Activate with: source .venv/[BIN]/activate + "###); + + let child = context.temp_dir.child("foo"); + + uv_snapshot!(context.filters(), context.init().current_dir(&context.temp_dir).arg(child.as_os_str()), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Initialized project `foo` at `[TEMP_DIR]/foo` + "###); + + let pyproject_toml = fs_err::read_to_string(child.join("pyproject.toml"))?; + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + pyproject_toml, @r###" + [project] + name = "foo" + version = "0.1.0" + description = "Add your description here" + readme = "README.md" + requires-python = ">=3.8" + dependencies = [] + "### + ); + }); + + Ok(()) +} + /// Run `uv init` from within an unmanaged project. #[test] fn init_unmanaged() -> Result<()> {