Skip to content

Commit

Permalink
Respect workspace-wide requires-python in interpreter selection
Browse files Browse the repository at this point in the history
  • Loading branch information
charliermarsh committed Jun 13, 2024
1 parent b43de79 commit 1d3c965
Show file tree
Hide file tree
Showing 2 changed files with 39 additions and 29 deletions.
14 changes: 2 additions & 12 deletions crates/uv/src/commands/project/lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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) {
Expand Down
54 changes: 37 additions & 17 deletions crates/uv/src/commands/project/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand All @@ -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;
};
Expand All @@ -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());
}
}
Expand All @@ -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(
Expand All @@ -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 \
Expand Down Expand Up @@ -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);
}

Expand Down

0 comments on commit 1d3c965

Please sign in to comment.