Skip to content

Commit

Permalink
Respect .python-version files and fetch manged toolchains in uv pro…
Browse files Browse the repository at this point in the history
…ject commands (#4361)

As in #4360, updates the uv project CLI to respect `.python-version`
files as default Python version requests. Additionally, updates project
interpreter discovery to fetch managed toolchains as in `uv venv
--preview`.
  • Loading branch information
zanieb committed Jun 18, 2024
1 parent 903dfc2 commit 1ce2147
Show file tree
Hide file tree
Showing 11 changed files with 118 additions and 49 deletions.
2 changes: 1 addition & 1 deletion crates/uv-client/src/base_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ pub struct BaseClientBuilder<'a> {
keyring: KeyringProviderType,
native_tls: bool,
retries: u32,
connectivity: Connectivity,
pub connectivity: Connectivity,
client: Option<Client>,
markers: Option<&'a MarkerEnvironment>,
platform: Option<&'a Platform>,
Expand Down
10 changes: 10 additions & 0 deletions crates/uv-client/src/registry_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -929,6 +929,16 @@ pub enum Connectivity {
Offline,
}

impl Connectivity {
pub fn is_online(&self) -> bool {
matches!(self, Self::Online)
}

pub fn is_offline(&self) -> bool {
matches!(self, Self::Offline)
}
}

#[cfg(test)]
mod tests {
use std::str::FromStr;
Expand Down
6 changes: 5 additions & 1 deletion crates/uv-toolchain/src/toolchain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,11 @@ impl Toolchain {
// Perform a find first
match Self::find(python.clone(), system, preview, cache) {
Ok(venv) => Ok(venv),
Err(Error::NotFound(_)) if system.is_allowed() && preview.is_enabled() => {
Err(Error::NotFound(_))
if system.is_allowed()
&& preview.is_enabled()
&& client_builder.connectivity.is_online() =>
{
debug!("Requested Python not found, checking for available download...");
let request = if let Some(request) = python {
request
Expand Down
11 changes: 10 additions & 1 deletion crates/uv/src/commands/project/add.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use uv_distribution::pyproject_mut::PyProjectTomlMut;
use uv_git::GitResolver;
use uv_requirements::{NamedRequirementsResolver, RequirementsSource, RequirementsSpecification};
use uv_resolver::{FlatIndex, InMemoryIndex, OptionsBuilder};
use uv_toolchain::ToolchainRequest;
use uv_types::{BuildIsolation, HashStrategy, InFlight};

use uv_cache::Cache;
Expand Down Expand Up @@ -41,7 +42,15 @@ pub(crate) async fn add(
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(), cache, printer)?;
let venv = project::init_environment(
project.workspace(),
python.as_deref().map(ToolchainRequest::parse),
connectivity,
native_tls,
cache,
printer,
)
.await?;

let client_builder = BaseClientBuilder::new()
.connectivity(connectivity)
Expand Down
12 changes: 10 additions & 2 deletions crates/uv/src/commands/project/lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use uv_resolver::{
ExcludeNewer, FlatIndex, InMemoryIndex, Lock, OptionsBuilder, PreReleaseMode, RequiresPython,
ResolutionMode,
};
use uv_toolchain::Interpreter;
use uv_toolchain::{Interpreter, ToolchainRequest};
use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy, InFlight};
use uv_warnings::warn_user;

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

// Find an interpreter for the project
let interpreter = project::find_interpreter(&workspace, python.as_deref(), cache, printer)?;
let interpreter = project::find_interpreter(
&workspace,
python.as_deref().map(ToolchainRequest::parse),
connectivity,
native_tls,
cache,
printer,
)
.await?;

// Perform the lock operation.
match do_lock(
Expand Down
87 changes: 51 additions & 36 deletions crates/uv/src/commands/project/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ use uv_installer::{SatisfiesResult, SitePackages};
use uv_requirements::{RequirementsSource, RequirementsSpecification};
use uv_resolver::{FlatIndex, InMemoryIndex, OptionsBuilder, RequiresPython};
use uv_toolchain::{
Interpreter, PythonEnvironment, SystemPython, Toolchain, ToolchainRequest, VersionRequest,
request_from_version_file, Interpreter, PythonEnvironment, SystemPython, Toolchain,
ToolchainRequest, VersionRequest,
};
use uv_types::{BuildIsolation, HashStrategy, InFlight};
use uv_warnings::warn_user;
Expand Down Expand Up @@ -99,7 +100,7 @@ pub(crate) fn find_environment(
/// Check if the given interpreter satisfies the project's requirements.
pub(crate) fn interpreter_meets_requirements(
interpreter: &Interpreter,
requested_python: Option<&str>,
requested_python: Option<&ToolchainRequest>,
requires_python: Option<&RequiresPython>,
cache: &Cache,
) -> bool {
Expand All @@ -108,8 +109,7 @@ pub(crate) fn interpreter_meets_requirements(
// 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 let Some(request) = requested_python {
if request.satisfied(interpreter, cache) {
debug!("Interpreter meets the requested python {}", request);
return true;
Expand All @@ -136,20 +136,36 @@ pub(crate) fn interpreter_meets_requirements(
}

/// Find the interpreter to use in the current project.
pub(crate) fn find_interpreter(
pub(crate) async fn find_interpreter(
workspace: &Workspace,
python: Option<&str>,
python_request: Option<ToolchainRequest>,
connectivity: Connectivity,
native_tls: bool,
cache: &Cache,
printer: Printer,
) -> Result<Interpreter, ProjectError> {
let requires_python = find_requires_python(workspace)?;

// (1) Explicit request from user
let python_request = if let Some(request) = python_request {
Some(request)
// (2) Request from `.python-version`
} else if let Some(request) = request_from_version_file().await? {
Some(request)
// (3) `Requires-Python` in `pyproject.toml`
} else {
requires_python
.as_ref()
.map(RequiresPython::specifiers)
.map(|specifiers| ToolchainRequest::Version(VersionRequest::Range(specifiers.clone())))
};

// Read from the virtual environment first
match find_environment(workspace, cache) {
Ok(venv) => {
if interpreter_meets_requirements(
venv.interpreter(),
python,
python_request.as_ref(),
requires_python.as_ref(),
cache,
) {
Expand All @@ -160,49 +176,47 @@ pub(crate) fn find_interpreter(
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
.as_ref()
.map(RequiresPython::specifiers)
.map(|specifiers| ToolchainRequest::Version(VersionRequest::Range(specifiers.clone()))))
{
Toolchain::find_requested(
&request,
SystemPython::Required,
PreviewMode::Enabled,
cache,
)
} else {
Toolchain::find(None, SystemPython::Required, PreviewMode::Enabled, cache)
}?
let client_builder = BaseClientBuilder::default()
.connectivity(connectivity)
.native_tls(native_tls);

// Locate the Python interpreter to use in the environment
let interpreter = Toolchain::find_or_fetch(
python_request,
SystemPython::Required,
PreviewMode::Enabled,
client_builder,
cache,
)
.await?
.into_interpreter();

writeln!(
printer.stderr(),
"Using Python {} interpreter at: {}",
interpreter.python_version(),
interpreter.sys_executable().user_display().cyan()
)?;

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 \
project of {}",
"The Python interpreter ({}) is incompatible with the project Python requirement {}",
interpreter.python_version(),
python.unwrap_or("(default)"),
requires_python
);
}
}

writeln!(
printer.stderr(),
"Using Python {} interpreter at: {}",
interpreter.python_version(),
interpreter.sys_executable().user_display().cyan()
)?;

Ok(interpreter)
}

/// Initialize a virtual environment for the current project.
pub(crate) fn init_environment(
pub(crate) async fn init_environment(
workspace: &Workspace,
python: Option<&str>,
python: Option<ToolchainRequest>,
connectivity: Connectivity,
native_tls: bool,
cache: &Cache,
printer: Printer,
) -> Result<PythonEnvironment, ProjectError> {
Expand All @@ -213,7 +227,7 @@ pub(crate) fn init_environment(
Ok(venv) => {
if interpreter_meets_requirements(
venv.interpreter(),
python,
python.as_ref(),
requires_python.as_ref(),
cache,
) {
Expand All @@ -234,7 +248,8 @@ pub(crate) fn init_environment(
};

// Find an interpreter to create the environment with
let interpreter = find_interpreter(workspace, python, cache, printer)?;
let interpreter =
find_interpreter(workspace, python, connectivity, native_tls, cache, printer).await?;

let venv = workspace.venv();
writeln!(
Expand Down
11 changes: 10 additions & 1 deletion crates/uv/src/commands/project/remove.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use uv_client::Connectivity;
use uv_configuration::{Concurrency, ExtrasSpecification, PreviewMode};
use uv_distribution::pyproject_mut::PyProjectTomlMut;
use uv_distribution::ProjectWorkspace;
use uv_toolchain::ToolchainRequest;
use uv_warnings::warn_user;

use crate::commands::pip::operations::Modifications;
Expand Down Expand Up @@ -81,7 +82,15 @@ pub(crate) async fn remove(
)?;

// Discover or create the virtual environment.
let venv = project::init_environment(project.workspace(), python.as_deref(), cache, printer)?;
let venv = project::init_environment(
project.workspace(),
python.as_deref().map(ToolchainRequest::parse),
connectivity,
native_tls,
cache,
printer,
)
.await?;

// Use the default settings.
let settings = ResolverSettings::default();
Expand Down
11 changes: 9 additions & 2 deletions crates/uv/src/commands/project/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,15 @@ pub(crate) async fn run(
} else {
ProjectWorkspace::discover(&std::env::current_dir()?, None).await?
};
let venv =
project::init_environment(project.workspace(), python.as_deref(), cache, printer)?;
let venv = project::init_environment(
project.workspace(),
python.as_deref().map(ToolchainRequest::parse),
connectivity,
native_tls,
cache,
printer,
)
.await?;

// Lock and sync the environment.
let lock = project::lock::do_lock(
Expand Down
12 changes: 10 additions & 2 deletions crates/uv/src/commands/project/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use uv_git::GitResolver;
use uv_installer::SitePackages;
use uv_normalize::PackageName;
use uv_resolver::{FlatIndex, InMemoryIndex, Lock};
use uv_toolchain::PythonEnvironment;
use uv_toolchain::{PythonEnvironment, ToolchainRequest};
use uv_types::{BuildIsolation, HashStrategy, InFlight};
use uv_warnings::warn_user;

Expand Down Expand Up @@ -49,7 +49,15 @@ 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(), cache, printer)?;
let venv = project::init_environment(
project.workspace(),
python.as_deref().map(ToolchainRequest::parse),
connectivity,
native_tls,
cache,
printer,
)
.await?;

// Read the lockfile.
let lock: Lock = {
Expand Down
2 changes: 0 additions & 2 deletions crates/uv/tests/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -302,9 +302,7 @@ impl TestContext {
.arg(self.cache_dir.path())
.env("VIRTUAL_ENV", self.venv.as_os_str())
.env("UV_TOOLCHAIN_DIR", "")
.env("UV_TEST_PYTHON_PATH", &self.python_path())
.env("UV_NO_WRAP", "1")
.env("UV_TOOLCHAIN_DIR", "")
.env("UV_TEST_PYTHON_PATH", &self.python_path())
.current_dir(&self.temp_dir);

Expand Down
3 changes: 2 additions & 1 deletion crates/uv/tests/lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1887,7 +1887,8 @@ fn lock_requires_python() -> Result<()> {
.collect();

// Install from the lockfile.
uv_snapshot!(filters, context38.sync(), @r###"
// Note we need `--offline` otherwise we'll just fetch a 3.12 interpreter!
uv_snapshot!(filters, context38.sync().arg("--offline"), @r###"
success: false
exit_code: 2
----- stdout -----
Expand Down

0 comments on commit 1ce2147

Please sign in to comment.