Skip to content

Commit

Permalink
Respect requires-python in uv lock (#4282)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
zanieb authored Jun 12, 2024
1 parent e6d0c4d commit 3910b7a
Show file tree
Hide file tree
Showing 6 changed files with 102 additions and 94 deletions.
8 changes: 1 addition & 7 deletions crates/uv/src/commands/project/add.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,7 @@ pub(crate) async fn add(
)?;

// Discover or create the virtual environment.
let venv = project::init_environment(
project.workspace(),
python.as_deref(),
preview,
cache,
printer,
)?;
let venv = project::init_environment(project.workspace(), python.as_deref(), cache, printer)?;

let index_locations = IndexLocations::default();
let upgrade = Upgrade::default();
Expand Down
25 changes: 2 additions & 23 deletions crates/uv/src/commands/project/lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use uv_git::GitResolver;
use uv_normalize::PackageName;
use uv_requirements::upgrade::{read_lockfile, LockedRequirements};
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_warnings::warn_user;

Expand All @@ -43,28 +43,7 @@ pub(crate) async fn lock(
let workspace = Workspace::discover(&std::env::current_dir()?, None).await?;

// Find an interpreter for the project
let interpreter = match project::find_environment(&workspace, cache) {
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()),
};
let interpreter = project::find_interpreter(&workspace, python.as_deref(), cache, printer)?;

// Perform the lock operation.
let root_project_name = workspace.root_member().and_then(|member| {
Expand Down
138 changes: 95 additions & 43 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;
use pep440_rs::{Version, VersionSpecifiers};
use uv_cache::Cache;
use uv_client::{BaseClientBuilder, Connectivity, RegistryClientBuilder};
use uv_configuration::{
Expand All @@ -21,7 +21,9 @@ use uv_git::GitResolver;
use uv_installer::{SatisfiesResult, SitePackages};
use uv_requirements::{RequirementsSource, RequirementsSpecification};
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_warnings::warn_user;

Expand Down Expand Up @@ -81,70 +83,83 @@ pub(crate) fn find_environment(
PythonEnvironment::from_root(workspace.venv(), cache)
}

/// Initialize a virtual environment for the current project.
pub(crate) fn init_environment(
/// Check if the given interpreter satisfies the project's requirements.
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,
python: Option<&str>,
preview: PreviewMode,
cache: &Cache,
printer: Printer,
) -> Result<PythonEnvironment, ProjectError> {
let venv = workspace.root().join(".venv");

) -> Result<Interpreter, ProjectError> {
let requires_python = workspace
.root_member()
.and_then(|root| root.project().requires_python.as_ref());

// Discover or create the virtual environment.
match PythonEnvironment::from_root(venv, cache) {
// Read from the virtual environment first
match find_environment(workspace, cache) {
Ok(venv) => {
// `--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?
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);
if interpreter_meets_requirements(venv.interpreter(), python, requires_python, cache) {
return Ok(venv.into_interpreter());
}

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()),
}
};

// Otherwise, find a system interpreter to use
let interpreter = if let Some(request) = python.map(ToolchainRequest::parse).or(requires_python
.map(|specifiers| ToolchainRequest::Version(VersionRequest::Range(specifiers.clone()))))
{
Toolchain::find_requested(
&request,
// Otherwise we'll try to use the venv we just deleted.
SystemPython::Required,
preview,
PreviewMode::Enabled,
cache,
)
} else {
Toolchain::find(
None,
// Otherwise we'll try to use the venv we just deleted.
SystemPython::Required,
preview,
cache,
)
Toolchain::find(None, SystemPython::Required, PreviewMode::Enabled, cache)
}?
.into_interpreter();

Expand All @@ -167,6 +182,43 @@ pub(crate) fn init_environment(
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();
writeln!(
printer.stderr(),
Expand Down
8 changes: 1 addition & 7 deletions crates/uv/src/commands/project/remove.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,7 @@ pub(crate) async fn remove(
)?;

// Discover or create the virtual environment.
let venv = project::init_environment(
project.workspace(),
python.as_deref(),
preview,
cache,
printer,
)?;
let venv = project::init_environment(project.workspace(), python.as_deref(), cache, printer)?;

let index_locations = IndexLocations::default();
let upgrade = Upgrade::None;
Expand Down
9 changes: 2 additions & 7 deletions crates/uv/src/commands/project/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,8 @@ pub(crate) async fn run(
} else {
ProjectWorkspace::discover(&std::env::current_dir()?, None).await?
};
let venv = project::init_environment(
project.workspace(),
python.as_deref(),
preview,
cache,
printer,
)?;
let venv =
project::init_environment(project.workspace(), python.as_deref(), cache, printer)?;

// Lock and sync the environment.
let root_project_name = project
Expand Down
8 changes: 1 addition & 7 deletions crates/uv/src/commands/project/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,7 @@ pub(crate) async fn sync(
let project = ProjectWorkspace::discover(&std::env::current_dir()?, None).await?;

// Discover or create the virtual environment.
let venv = project::init_environment(
project.workspace(),
python.as_deref(),
preview,
cache,
printer,
)?;
let venv = project::init_environment(project.workspace(), python.as_deref(), cache, printer)?;

// Read the lockfile.
let lock: Lock = {
Expand Down

0 comments on commit 3910b7a

Please sign in to comment.