Skip to content

Commit

Permalink
Ignore virtual environments in parent directories when choosing Pytho…
Browse files Browse the repository at this point in the history
…n version for new projects (#9075)

`uv init` shouldn't have been using `EnvironmentPreference::Any` for
discovery of a Python interpreter, it seems like an oversight that it
was reading from virtual environments. I changed it to
`EnvironmentPreference::OnlySystem` so we'll use the first Python on the
`PATH` instead. However, I think we actually do want to respect a
virtual environment's Python version if it's in the target project
directory already, so I've implemented that as well.

Closes #9072
Closes #8092
  • Loading branch information
zanieb authored Nov 13, 2024
1 parent 95e7d87 commit cb430b8
Show file tree
Hide file tree
Showing 2 changed files with 127 additions and 8 deletions.
38 changes: 30 additions & 8 deletions crates/uv/src/commands/project/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand Down
97 changes: 97 additions & 0 deletions crates/uv/tests/it/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<()> {
Expand Down

0 comments on commit cb430b8

Please sign in to comment.