Respect workspace-wide requires-python in interpreter selection (#4298)

## Summary

Closes https://github.com/astral-sh/uv/issues/4296.

## Test Plan

Ran `cargo run lock --verbose` from
`scripts/workspaces/albatross-virtual-workspace`:

```
DEBUG uv 0.2.11 (ef3bc1612 2024-06-12)
warning: `uv lock` is experimental and may change without warning.
DEBUG Found workspace root: `/Users/crmarsh/workspace/puffin/scripts/workspaces/albatross-virtual-workspace`
DEBUG Adding discovered workspace member: /Users/crmarsh/workspace/puffin/scripts/workspaces/albatross-virtual-workspace/packages/albatross
DEBUG Adding discovered workspace member: /Users/crmarsh/workspace/puffin/scripts/workspaces/albatross-virtual-workspace/packages/bird-feeder
DEBUG Adding discovered workspace member: /Users/crmarsh/workspace/puffin/scripts/workspaces/albatross-virtual-workspace/packages/seeds
DEBUG Searching for Python >=3.12 in search path or managed toolchains
DEBUG Searching for managed toolchains at `/Users/crmarsh/Library/Application Support/uv/toolchains`
DEBUG Found managed toolchain `cpython-3.12.3-macos-aarch64-none`
DEBUG Found CPython 3.12.3 at `/Users/crmarsh/Library/Application Support/uv/toolchains/cpython-3.12.3-macos-aarch64-none/install/bin/python3` (managed toolchains)
Using Python 3.12.3 interpreter at: /Users/crmarsh/Library/Application Support/uv/toolchains/cpython-3.12.3-macos-aarch64-none/install/bin/python3
```
This commit is contained in:
Charlie Marsh 2024-06-13 05:55:56 -07:00 committed by GitHub
parent b43de79275
commit 5d1305aa6b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 39 additions and 29 deletions

View file

@ -20,7 +20,7 @@ use uv_toolchain::Interpreter;
use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy, InFlight};
use uv_warnings::warn_user;
use crate::commands::project::ProjectError;
use crate::commands::project::{find_requires_python, ProjectError};
use crate::commands::{pip, project, ExitStatus};
use crate::printer::Printer;
@ -106,17 +106,7 @@ pub(super) async fn do_lock(
// Determine the supported Python range. If no range is defined, and warn and default to the
// current minor version.
//
// For a workspace, we compute the union of all workspace requires-python values, ensuring we
// keep track of `None` vs. a full range.
let requires_python =
RequiresPython::union(workspace.packages().values().filter_map(|member| {
member
.pyproject_toml()
.project
.as_ref()
.and_then(|project| project.requires_python.as_ref())
}))?;
let requires_python = find_requires_python(workspace)?;
let requires_python = if let Some(requires_python) = requires_python {
if matches!(requires_python.bound(), Bound::Unbounded) {

View file

@ -7,7 +7,7 @@ use tracing::debug;
use distribution_types::{IndexLocations, Resolution};
use install_wheel_rs::linker::LinkMode;
use pep440_rs::{Version, VersionSpecifiers};
use pep440_rs::Version;
use uv_cache::Cache;
use uv_client::{BaseClientBuilder, Connectivity, RegistryClientBuilder};
use uv_configuration::{
@ -75,6 +75,22 @@ pub(crate) enum ProjectError {
RequiresPython(#[from] uv_resolver::RequiresPythonError),
}
/// Compute the `Requires-Python` bound for the [`Workspace`].
///
/// For a [`Workspace`] with multiple packages, the `Requires-Python` bound is the union of the
/// `Requires-Python` bounds of all the packages.
pub(crate) fn find_requires_python(
workspace: &Workspace,
) -> Result<Option<RequiresPython>, uv_resolver::RequiresPythonError> {
RequiresPython::union(workspace.packages().values().filter_map(|member| {
member
.pyproject_toml()
.project
.as_ref()
.and_then(|project| project.requires_python.as_ref())
}))
}
/// Find the virtual environment for the current project.
pub(crate) fn find_environment(
workspace: &Workspace,
@ -87,7 +103,7 @@ pub(crate) fn find_environment(
pub(crate) fn interpreter_meets_requirements(
interpreter: &Interpreter,
requested_python: Option<&str>,
requires_python: Option<&VersionSpecifiers>,
requires_python: Option<&RequiresPython>,
cache: &Cache,
) -> bool {
// `--python` has highest precedence, after that we check `requires_python` from
@ -108,16 +124,12 @@ pub(crate) fn interpreter_meets_requirements(
if let Some(requires_python) = requires_python {
if requires_python.contains(interpreter.python_version()) {
debug!(
"Interpreter meets the project `Requires-Python` constraint {}",
requires_python
);
debug!("Interpreter meets the project `Requires-Python` constraint {requires_python}");
return true;
}
debug!(
"Interpreter does not meet the project `Requires-Python` constraint {}",
requires_python
"Interpreter does not meet the project `Requires-Python` constraint {requires_python}"
);
return false;
};
@ -133,14 +145,17 @@ pub(crate) fn find_interpreter(
cache: &Cache,
printer: Printer,
) -> Result<Interpreter, ProjectError> {
let requires_python = workspace
.root_member()
.and_then(|root| root.project().requires_python.as_ref());
let requires_python = find_requires_python(workspace)?;
// Read from the virtual environment first
match find_environment(workspace, cache) {
Ok(venv) => {
if interpreter_meets_requirements(venv.interpreter(), python, requires_python, cache) {
if interpreter_meets_requirements(
venv.interpreter(),
python,
requires_python.as_ref(),
cache,
) {
return Ok(venv.into_interpreter());
}
}
@ -150,6 +165,8 @@ pub(crate) fn find_interpreter(
// Otherwise, find a system interpreter to use
let interpreter = if let Some(request) = python.map(ToolchainRequest::parse).or(requires_python
.as_ref()
.map(RequiresPython::specifiers)
.map(|specifiers| ToolchainRequest::Version(VersionRequest::Range(specifiers.clone()))))
{
Toolchain::find_requested(
@ -163,7 +180,7 @@ pub(crate) fn find_interpreter(
}?
.into_interpreter();
if let Some(requires_python) = requires_python {
if let Some(requires_python) = requires_python.as_ref() {
if !requires_python.contains(interpreter.python_version()) {
warn_user!(
"The Python {} you requested with {} is incompatible with the requirement of the \
@ -192,14 +209,17 @@ pub(crate) fn init_environment(
cache: &Cache,
printer: Printer,
) -> Result<PythonEnvironment, ProjectError> {
let requires_python = workspace
.root_member()
.and_then(|root| root.project().requires_python.as_ref());
let requires_python = find_requires_python(workspace)?;
// Check if the environment exists and is sufficient
match find_environment(workspace, cache) {
Ok(venv) => {
if interpreter_meets_requirements(venv.interpreter(), python, requires_python, cache) {
if interpreter_meets_requirements(
venv.interpreter(),
python,
requires_python.as_ref(),
cache,
) {
return Ok(venv);
}