Respect requires-python in uv lock (#4282)

We weren't using the common interface in `uv lock` because it didn't
support finding an interpreter without touching the virtual environment.
Here I refactor the project interface to support what we need and update
`uv lock` to use the shared implementation.
This commit is contained in:
Zanie Blue 2024-06-12 14:19:00 -04:00 committed by GitHub
parent e6d0c4d9fe
commit 3910b7a90c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 102 additions and 94 deletions

View file

@ -41,13 +41,7 @@ pub(crate) async fn add(
)?; )?;
// Discover or create the virtual environment. // Discover or create the virtual environment.
let venv = project::init_environment( let venv = project::init_environment(project.workspace(), python.as_deref(), cache, printer)?;
project.workspace(),
python.as_deref(),
preview,
cache,
printer,
)?;
let index_locations = IndexLocations::default(); let index_locations = IndexLocations::default();
let upgrade = Upgrade::default(); let upgrade = Upgrade::default();

View file

@ -16,7 +16,7 @@ use uv_git::GitResolver;
use uv_normalize::PackageName; use uv_normalize::PackageName;
use uv_requirements::upgrade::{read_lockfile, LockedRequirements}; use uv_requirements::upgrade::{read_lockfile, LockedRequirements};
use uv_resolver::{ExcludeNewer, FlatIndex, InMemoryIndex, Lock, OptionsBuilder, RequiresPython}; use uv_resolver::{ExcludeNewer, FlatIndex, InMemoryIndex, Lock, OptionsBuilder, RequiresPython};
use uv_toolchain::{Interpreter, SystemPython, Toolchain, ToolchainRequest}; use uv_toolchain::Interpreter;
use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy, InFlight}; use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy, InFlight};
use uv_warnings::warn_user; use uv_warnings::warn_user;
@ -43,28 +43,7 @@ pub(crate) async fn lock(
let workspace = Workspace::discover(&std::env::current_dir()?, None).await?; let workspace = Workspace::discover(&std::env::current_dir()?, None).await?;
// Find an interpreter for the project // Find an interpreter for the project
let interpreter = match project::find_environment(&workspace, cache) { let interpreter = project::find_interpreter(&workspace, python.as_deref(), cache, printer)?;
Ok(environment) => {
let interpreter = environment.into_interpreter();
if let Some(python) = python.as_deref() {
let request = ToolchainRequest::parse(python);
if request.satisfied(&interpreter, cache) {
interpreter
} else {
let request = ToolchainRequest::parse(python);
Toolchain::find_requested(&request, SystemPython::Allowed, preview, cache)?
.into_interpreter()
}
} else {
interpreter
}
}
Err(uv_toolchain::Error::NotFound(_)) => {
Toolchain::find(python.as_deref(), SystemPython::Allowed, preview, cache)?
.into_interpreter()
}
Err(err) => return Err(err.into()),
};
// Perform the lock operation. // Perform the lock operation.
let root_project_name = workspace.root_member().and_then(|member| { let root_project_name = workspace.root_member().and_then(|member| {

View file

@ -7,7 +7,7 @@ use tracing::debug;
use distribution_types::{IndexLocations, Resolution}; use distribution_types::{IndexLocations, Resolution};
use install_wheel_rs::linker::LinkMode; use install_wheel_rs::linker::LinkMode;
use pep440_rs::Version; use pep440_rs::{Version, VersionSpecifiers};
use uv_cache::Cache; use uv_cache::Cache;
use uv_client::{BaseClientBuilder, Connectivity, RegistryClientBuilder}; use uv_client::{BaseClientBuilder, Connectivity, RegistryClientBuilder};
use uv_configuration::{ use uv_configuration::{
@ -21,7 +21,9 @@ use uv_git::GitResolver;
use uv_installer::{SatisfiesResult, SitePackages}; use uv_installer::{SatisfiesResult, SitePackages};
use uv_requirements::{RequirementsSource, RequirementsSpecification}; use uv_requirements::{RequirementsSource, RequirementsSpecification};
use uv_resolver::{FlatIndex, InMemoryIndex, Options, RequiresPython}; use uv_resolver::{FlatIndex, InMemoryIndex, Options, RequiresPython};
use uv_toolchain::{PythonEnvironment, SystemPython, Toolchain, ToolchainRequest, VersionRequest}; use uv_toolchain::{
Interpreter, PythonEnvironment, SystemPython, Toolchain, ToolchainRequest, VersionRequest,
};
use uv_types::{BuildIsolation, HashStrategy, InFlight}; use uv_types::{BuildIsolation, HashStrategy, InFlight};
use uv_warnings::warn_user; use uv_warnings::warn_user;
@ -81,70 +83,83 @@ pub(crate) fn find_environment(
PythonEnvironment::from_root(workspace.venv(), cache) PythonEnvironment::from_root(workspace.venv(), cache)
} }
/// Initialize a virtual environment for the current project. /// Check if the given interpreter satisfies the project's requirements.
pub(crate) fn init_environment( pub(crate) fn interpreter_meets_requirements(
interpreter: &Interpreter,
requested_python: Option<&str>,
requires_python: Option<&VersionSpecifiers>,
cache: &Cache,
) -> bool {
// `--python` has highest precedence, after that we check `requires_python` from
// `pyproject.toml`. If `--python` and `requires_python` are mutually incompatible,
// we'll fail at the build or at last the install step when we aren't able to install
// the editable wheel for the current project into the venv.
// TODO(konsti): Do we want to support a workspace python version requirement?
if let Some(python) = requested_python {
let request = ToolchainRequest::parse(python);
if request.satisfied(interpreter, cache) {
debug!("Interpreter meets the requested python {}", request);
return true;
}
debug!("Interpreter does not meet the request {}", request);
return false;
};
if let Some(requires_python) = requires_python {
if requires_python.contains(interpreter.python_version()) {
debug!(
"Interpreter meets the project `Requires-Python` constraint {}",
requires_python
);
return true;
}
debug!(
"Interpreter does not meet the project `Requires-Python` constraint {}",
requires_python
);
return false;
};
// No requirement to check
true
}
/// Find the interpreter to use in the current project.
pub(crate) fn find_interpreter(
workspace: &Workspace, workspace: &Workspace,
python: Option<&str>, python: Option<&str>,
preview: PreviewMode,
cache: &Cache, cache: &Cache,
printer: Printer, printer: Printer,
) -> Result<PythonEnvironment, ProjectError> { ) -> Result<Interpreter, ProjectError> {
let venv = workspace.root().join(".venv");
let requires_python = workspace let requires_python = workspace
.root_member() .root_member()
.and_then(|root| root.project().requires_python.as_ref()); .and_then(|root| root.project().requires_python.as_ref());
// Discover or create the virtual environment. // Read from the virtual environment first
match PythonEnvironment::from_root(venv, cache) { match find_environment(workspace, cache) {
Ok(venv) => { Ok(venv) => {
// `--python` has highest precedence, after that we check `requires_python` from if interpreter_meets_requirements(venv.interpreter(), python, requires_python, cache) {
// `pyproject.toml`. If `--python` and `requires_python` are mutually incompatible, return Ok(venv.into_interpreter());
// we'll fail at the build or at last the install step when we aren't able to install
// the editable wheel for the current project into the venv.
// TODO(konsti): Do we want to support a workspace python version requirement?
let is_satisfied = if let Some(python) = python {
ToolchainRequest::parse(python).satisfied(venv.interpreter(), cache)
} else if let Some(requires_python) = requires_python {
requires_python.contains(venv.interpreter().python_version())
} else {
true
};
if is_satisfied {
return Ok(venv);
} }
writeln!(
printer.stderr(),
"Removing virtual environment at: {}",
venv.root().user_display().cyan()
)?;
fs_err::remove_dir_all(venv.root())
.context("Failed to remove existing virtual environment")?;
} }
Err(uv_toolchain::Error::NotFound(_)) => {} Err(uv_toolchain::Error::NotFound(_)) => {}
Err(e) => return Err(e.into()), Err(e) => return Err(e.into()),
} };
// Otherwise, find a system interpreter to use
let interpreter = if let Some(request) = python.map(ToolchainRequest::parse).or(requires_python let interpreter = if let Some(request) = python.map(ToolchainRequest::parse).or(requires_python
.map(|specifiers| ToolchainRequest::Version(VersionRequest::Range(specifiers.clone())))) .map(|specifiers| ToolchainRequest::Version(VersionRequest::Range(specifiers.clone()))))
{ {
Toolchain::find_requested( Toolchain::find_requested(
&request, &request,
// Otherwise we'll try to use the venv we just deleted.
SystemPython::Required, SystemPython::Required,
preview, PreviewMode::Enabled,
cache, cache,
) )
} else { } else {
Toolchain::find( Toolchain::find(None, SystemPython::Required, PreviewMode::Enabled, cache)
None,
// Otherwise we'll try to use the venv we just deleted.
SystemPython::Required,
preview,
cache,
)
}? }?
.into_interpreter(); .into_interpreter();
@ -167,6 +182,43 @@ pub(crate) fn init_environment(
interpreter.sys_executable().user_display().cyan() interpreter.sys_executable().user_display().cyan()
)?; )?;
Ok(interpreter)
}
/// Initialize a virtual environment for the current project.
pub(crate) fn init_environment(
workspace: &Workspace,
python: Option<&str>,
cache: &Cache,
printer: Printer,
) -> Result<PythonEnvironment, ProjectError> {
let requires_python = workspace
.root_member()
.and_then(|root| root.project().requires_python.as_ref());
// 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) {
return Ok(venv);
}
// Remove the existing virtual environment if it doesn't meet the requirements
writeln!(
printer.stderr(),
"Removing virtual environment at: {}",
venv.root().user_display().cyan()
)?;
fs_err::remove_dir_all(venv.root())
.context("Failed to remove existing virtual environment")?;
}
Err(uv_toolchain::Error::NotFound(_)) => {}
Err(e) => return Err(e.into()),
};
// Find an interpreter to create the environment with
let interpreter = find_interpreter(workspace, python, cache, printer)?;
let venv = workspace.venv(); let venv = workspace.venv();
writeln!( writeln!(
printer.stderr(), printer.stderr(),

View file

@ -44,13 +44,7 @@ pub(crate) async fn remove(
)?; )?;
// Discover or create the virtual environment. // Discover or create the virtual environment.
let venv = project::init_environment( let venv = project::init_environment(project.workspace(), python.as_deref(), cache, printer)?;
project.workspace(),
python.as_deref(),
preview,
cache,
printer,
)?;
let index_locations = IndexLocations::default(); let index_locations = IndexLocations::default();
let upgrade = Upgrade::None; let upgrade = Upgrade::None;

View file

@ -62,13 +62,8 @@ pub(crate) async fn run(
} else { } else {
ProjectWorkspace::discover(&std::env::current_dir()?, None).await? ProjectWorkspace::discover(&std::env::current_dir()?, None).await?
}; };
let venv = project::init_environment( let venv =
project.workspace(), project::init_environment(project.workspace(), python.as_deref(), cache, printer)?;
python.as_deref(),
preview,
cache,
printer,
)?;
// Lock and sync the environment. // Lock and sync the environment.
let root_project_name = project let root_project_name = project

View file

@ -43,13 +43,7 @@ pub(crate) async fn sync(
let project = ProjectWorkspace::discover(&std::env::current_dir()?, None).await?; let project = ProjectWorkspace::discover(&std::env::current_dir()?, None).await?;
// Discover or create the virtual environment. // Discover or create the virtual environment.
let venv = project::init_environment( let venv = project::init_environment(project.workspace(), python.as_deref(), cache, printer)?;
project.workspace(),
python.as_deref(),
preview,
cache,
printer,
)?;
// Read the lockfile. // Read the lockfile.
let lock: Lock = { let lock: Lock = {