Skip to content

Commit

Permalink
Detect python version from python project by default in uv venv (#5592
Browse files Browse the repository at this point in the history
)

<!--
Thank you for contributing to uv! To help us out with reviewing, please
consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title?
- Does this pull request include references to any relevant issues?
-->

## Summary

<!-- What's the purpose of the change? What does it do, and why? -->

`uv venv` should support adopting python version specified in
`requires-python` from `pyproject.toml`. This allows customization on
the venv setup when syncing from python project.

Closes #5552.

It also serves as a workaround to close
#5258.

## Test Plan

<!-- How was it tested? -->

1. Run `uv venv` in folder with `pyroject.toml` specifying
`requries-python = "<3.10"`. Python 3.9 is selected for venv.
2. Change to `requries-python = "<3.11"` and run `uv venv` again. Python
3.10 is selected now.
3. Switch to a folder without `pyproject.toml` then run `uv venv`.
Python 3.12 is selected now.

---------

Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
Co-authored-by: Zanie Blue <contact@zanie.dev>
  • Loading branch information
3 people authored Jul 31, 2024
1 parent fbff1ba commit 0dcec9e
Show file tree
Hide file tree
Showing 2 changed files with 211 additions and 8 deletions.
40 changes: 32 additions & 8 deletions crates/uv/src/commands/venv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,18 @@ use uv_configuration::{
NoBuild, PreviewMode, SetupPyStrategy,
};
use uv_dispatch::BuildDispatch;
use uv_fs::Simplified;
use uv_fs::{Simplified, CWD};
use uv_python::{
request_from_version_file, EnvironmentPreference, PythonFetch, PythonInstallation,
PythonPreference, PythonRequest,
PythonPreference, PythonRequest, VersionRequest,
};
use uv_resolver::{ExcludeNewer, FlatIndex};
use uv_resolver::{ExcludeNewer, FlatIndex, RequiresPython};
use uv_shell::Shell;
use uv_types::{BuildContext, BuildIsolation, HashStrategy};
use uv_warnings::warn_user_once;
use uv_workspace::{DiscoveryOptions, VirtualProject, WorkspaceError};

use crate::commands::project::find_requires_python;
use crate::commands::reporters::PythonDownloadReporter;
use crate::commands::{pip, ExitStatus, SharedState};
use crate::printer::Printer;
Expand Down Expand Up @@ -130,23 +132,45 @@ async fn venv_impl(
printer: Printer,
relocatable: bool,
) -> miette::Result<ExitStatus> {
if preview.is_disabled() && relocatable {
warn_user_once!("`--relocatable` is experimental and may change without warning");
}

let client_builder = BaseClientBuilder::default()
.connectivity(connectivity)
.native_tls(native_tls);

let client_builder_clone = client_builder.clone();

let reporter = PythonDownloadReporter::single(printer);

// (1) Explicit request from user
let mut interpreter_request = python_request.map(PythonRequest::parse);

// (2) Request from `.python-version`
if preview.is_enabled() && interpreter_request.is_none() {
interpreter_request =
request_from_version_file(&std::env::current_dir().into_diagnostic()?)
.await
.into_diagnostic()?;
}
if preview.is_disabled() && relocatable {
warn_user_once!("`--relocatable` is experimental and may change without warning");

// (3) `Requires-Python` in `pyproject.toml`
if preview.is_enabled() && interpreter_request.is_none() {
let project = match VirtualProject::discover(&CWD, &DiscoveryOptions::default()).await {
Ok(project) => Some(project),
Err(WorkspaceError::MissingPyprojectToml) => None,
Err(WorkspaceError::NonWorkspace(_)) => None,
Err(err) => return Err(err).into_diagnostic(),
};

if let Some(project) = project {
interpreter_request = find_requires_python(project.workspace())
.into_diagnostic()?
.as_ref()
.map(RequiresPython::specifiers)
.map(|specifiers| {
PythonRequest::Version(VersionRequest::Range(specifiers.clone()))
});
}
}

// Locate the Python interpreter to use in the environment
Expand Down Expand Up @@ -217,7 +241,7 @@ async fn venv_impl(
}

// Instantiate a client.
let client = RegistryClientBuilder::from(client_builder_clone)
let client = RegistryClientBuilder::from(client_builder)
.cache(cache.clone())
.index_urls(index_locations.index_urls())
.index_strategy(index_strategy)
Expand Down
179 changes: 179 additions & 0 deletions crates/uv/tests/venv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
use anyhow::Result;
use assert_cmd::prelude::*;
use assert_fs::prelude::*;
use indoc::indoc;
use uv_python::{PYTHON_VERSIONS_FILENAME, PYTHON_VERSION_FILENAME};

use crate::common::{uv_snapshot, TestContext};
Expand Down Expand Up @@ -171,6 +172,184 @@ fn create_venv_reads_request_from_python_versions_file() {
context.venv.assert(predicates::path::is_dir());
}

#[test]
fn create_venv_respects_pyproject_requires_python() -> Result<()> {
let context = TestContext::new_with_versions(&["3.11", "3.9", "3.10", "3.12"]);

// Without a Python requirement, we use the first on the PATH
uv_snapshot!(context.filters(), context.venv()
.arg("--preview"), @r#"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Using Python 3.11.[X] interpreter at: [PYTHON-3.11]
Creating virtualenv at: .venv
Activate with: source .venv/bin/activate
"#
);

// With `requires-python = "<3.11"`, we prefer the first available version
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(indoc! { r#"
[project]
name = "foo"
version = "1.0.0"
requires-python = "<3.11"
dependencies = []
"#
})?;

uv_snapshot!(context.filters(), context.venv()
.arg("--preview"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Using Python 3.9.[X] interpreter at: [PYTHON-3.9]
Creating virtualenv at: .venv
Activate with: source .venv/bin/activate
"###
);

// With `requires-python = "==3.11.*"`, we prefer exact version (3.11)
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(indoc! { r#"
[project]
name = "foo"
version = "1.0.0"
requires-python = "==3.11.*"
dependencies = []
"#
})?;

uv_snapshot!(context.filters(), context.venv()
.arg("--preview"), @r#"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Using Python 3.11.[X] interpreter at: [PYTHON-3.11]
Creating virtualenv at: .venv
Activate with: source .venv/bin/activate
"#
);

// With `requires-python = ">=3.11,<3.12"`, we prefer exact version (3.11)
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(indoc! { r#"
[project]
name = "foo"
version = "1.0.0"
requires-python = ">=3.11,<3.12"
dependencies = []
"#
})?;

uv_snapshot!(context.filters(), context.venv()
.arg("--preview"), @r#"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Using Python 3.11.[X] interpreter at: [PYTHON-3.11]
Creating virtualenv at: .venv
Activate with: source .venv/bin/activate
"#
);

// With `requires-python = ">=3.10"`, we prefer first compatible version (3.11)
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(indoc! { r#"
[project]
name = "foo"
version = "1.0.0"
requires-python = ">=3.11"
dependencies = []
"#
})?;

// With `requires-python = ">=3.11"`, we prefer first compatible version (3.11)
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(indoc! { r#"
[project]
name = "foo"
version = "1.0.0"
requires-python = ">=3.11"
dependencies = []
"#
})?;

uv_snapshot!(context.filters(), context.venv()
.arg("--preview"), @r#"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Using Python 3.11.[X] interpreter at: [PYTHON-3.11]
Creating virtualenv at: .venv
Activate with: source .venv/bin/activate
"#
);

// With `requires-python = ">3.11"`, we prefer first compatible version (3.11)
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(indoc! { r#"
[project]
name = "foo"
version = "1.0.0"
requires-python = ">3.11"
dependencies = []
"#
})?;

uv_snapshot!(context.filters(), context.venv()
.arg("--preview"), @r#"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Using Python 3.11.[X] interpreter at: [PYTHON-3.11]
Creating virtualenv at: .venv
Activate with: source .venv/bin/activate
"#
);

// With `requires-python = ">=3.12"`, we prefer first compatible version (3.12)
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(indoc! { r#"
[project]
name = "foo"
version = "1.0.0"
requires-python = ">=3.12"
dependencies = []
"#
})?;

uv_snapshot!(context.filters(), context.venv()
.arg("--preview"), @r#"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Using Python 3.12.[X] interpreter at: [PYTHON-3.12]
Creating virtualenv at: .venv
Activate with: source .venv/bin/activate
"#
);

context.venv.assert(predicates::path::is_dir());

Ok(())
}

#[test]
fn create_venv_explicit_request_takes_priority_over_python_version_file() {
let context = TestContext::new_with_versions(&["3.11", "3.12"]);
Expand Down

0 comments on commit 0dcec9e

Please sign in to comment.