Skip to content

Commit

Permalink
Recreate project environment if --python or requires-python doesn…
Browse files Browse the repository at this point in the history
…'t match (#3945)

Fixes #4131
Fixes #3895
  • Loading branch information
konstin authored Jun 10, 2024
1 parent 125a4b2 commit 90947a9
Show file tree
Hide file tree
Showing 10 changed files with 387 additions and 85 deletions.
115 changes: 101 additions & 14 deletions crates/uv-toolchain/src/discovery.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
use std::borrow::Cow;
use std::collections::HashSet;
use std::fmt::{self, Formatter};
use std::num::ParseIntError;
use std::{env, io};
use std::{path::Path, path::PathBuf, str::FromStr};

use itertools::Itertools;
use same_file::is_same_file;
use thiserror::Error;
use tracing::{debug, instrument, trace};
use which::which;

use uv_cache::Cache;
use uv_configuration::PreviewMode;
use uv_fs::Simplified;
use uv_warnings::warn_user_once;
use which::which;

use crate::implementation::{ImplementationName, LenientImplementationName};
use crate::interpreter::Error as InterpreterError;
Expand All @@ -17,17 +26,10 @@ use crate::virtualenv::{
virtualenv_python_executable,
};
use crate::{Interpreter, PythonVersion};
use std::borrow::Cow;

use std::collections::HashSet;
use std::fmt::{self, Formatter};
use std::num::ParseIntError;
use std::{env, io};
use std::{path::Path, path::PathBuf, str::FromStr};

/// A request to find a Python toolchain.
///
/// See [`InterpreterRequest::from_str`].
/// See [`ToolchainRequest::from_str`].
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum ToolchainRequest {
/// Use any discovered Python toolchain
Expand Down Expand Up @@ -699,7 +701,7 @@ pub fn find_best_toolchain(
debug!("Looking for Python toolchain with any version");
let request = ToolchainRequest::Any;
Ok(find_toolchain(
// TODO(zanieb): Add a dedicated `Default` variant to `InterpreterRequest`
// TODO(zanieb): Add a dedicated `Default` variant to `ToolchainRequest`
&request, system, &sources, cache,
)?
.map_err(|err| {
Expand Down Expand Up @@ -860,7 +862,7 @@ fn is_windows_store_shim(_path: &Path) -> bool {
impl ToolchainRequest {
/// Create a request from a string.
///
/// This cannot fail, which means weird inputs will be parsed as [`InterpreterRequest::File`] or [`InterpreterRequest::ExecutableName`].
/// This cannot fail, which means weird inputs will be parsed as [`ToolchainRequest::File`] or [`ToolchainRequest::ExecutableName`].
pub fn parse(value: &str) -> Self {
// e.g. `3.12.1`
if let Ok(version) = VersionRequest::from_str(value) {
Expand Down Expand Up @@ -934,6 +936,93 @@ impl ToolchainRequest {
// e.g. foo.exe
Self::ExecutableName(value.to_string())
}

/// Check if a given interpreter satisfies the interpreter request.
pub fn satisfied(&self, interpreter: &Interpreter, cache: &Cache) -> bool {
/// Returns `true` if the two paths refer to the same interpreter executable.
fn is_same_executable(path1: &Path, path2: &Path) -> bool {
path1 == path2 || is_same_file(path1, path2).unwrap_or(false)
}

match self {
ToolchainRequest::Any => true,
ToolchainRequest::Version(version_request) => {
version_request.matches_interpreter(interpreter)
}
ToolchainRequest::Directory(directory) => {
// `sys.prefix` points to the venv root.
is_same_executable(directory, interpreter.sys_prefix())
}
ToolchainRequest::File(file) => {
// The interpreter satisfies the request both if it is the venv...
if is_same_executable(interpreter.sys_executable(), file) {
return true;
}
// ...or if it is the base interpreter the venv was created from.
if interpreter
.sys_base_executable()
.is_some_and(|sys_base_executable| {
is_same_executable(sys_base_executable, file)
})
{
return true;
}
// ...or, on Windows, if both interpreters have the same base executable. On
// Windows, interpreters are copied rather than symlinked, so a virtual environment
// created from within a virtual environment will _not_ evaluate to the same
// `sys.executable`, but will have the same `sys._base_executable`.
if cfg!(windows) {
if let Ok(file_interpreter) = Interpreter::query(file, cache) {
if let (Some(file_base), Some(interpreter_base)) = (
file_interpreter.sys_base_executable(),
interpreter.sys_base_executable(),
) {
if is_same_executable(file_base, interpreter_base) {
return true;
}
}
}
}
false
}
ToolchainRequest::ExecutableName(name) => {
// First, see if we have a match in the venv ...
if interpreter
.sys_executable()
.file_name()
.is_some_and(|filename| filename == name.as_str())
{
return true;
}
// ... or the venv's base interpreter (without performing IO), if that fails, ...
if interpreter
.sys_base_executable()
.and_then(|executable| executable.file_name())
.is_some_and(|file_name| file_name == name.as_str())
{
return true;
}
// ... check in `PATH`. The name we find here does not need to be the
// name we install, so we can find `foopython` here which got installed as `python`.
if which(name)
.ok()
.as_ref()
.and_then(|executable| executable.file_name())
.is_some_and(|file_name| file_name == name.as_str())
{
return true;
}
false
}
ToolchainRequest::Implementation(implementation) => {
interpreter.implementation_name() == implementation.as_str()
}
ToolchainRequest::ImplementationVersion(implementation, version) => {
version.matches_interpreter(interpreter)
&& interpreter.implementation_name() == implementation.as_str()
}
}
}
}

impl VersionRequest {
Expand Down Expand Up @@ -1353,12 +1442,10 @@ impl fmt::Display for ToolchainSources {

#[cfg(test)]
mod tests {

use std::{path::PathBuf, str::FromStr};

use test_log::test;

use assert_fs::{prelude::*, TempDir};
use test_log::test;

use crate::{
discovery::{ToolchainRequest, VersionRequest},
Expand Down
21 changes: 18 additions & 3 deletions crates/uv/src/commands/project/lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,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, Toolchain};
use uv_toolchain::{Interpreter, SystemPython, Toolchain, ToolchainRequest};
use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy, InFlight};
use uv_warnings::warn_user;

Expand All @@ -28,6 +28,7 @@ pub(crate) async fn lock(
index_locations: IndexLocations,
upgrade: Upgrade,
exclude_newer: Option<ExcludeNewer>,
python: Option<String>,
preview: PreviewMode,
cache: &Cache,
printer: Printer,
Expand All @@ -41,9 +42,23 @@ pub(crate) async fn lock(

// Find an interpreter for the project
let interpreter = match project::find_environment(&workspace, cache) {
Ok(environment) => environment.into_interpreter(),
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 {
Toolchain::find_requested(python, SystemPython::Allowed, preview, cache)?
.into_interpreter()
}
} else {
interpreter
}
}
Err(uv_toolchain::Error::NotFound(_)) => {
Toolchain::find_default(PreviewMode::Enabled, cache)?.into_interpreter()
Toolchain::find(python.as_deref(), SystemPython::Allowed, preview, cache)?
.into_interpreter()
}
Err(err) => return Err(err.into()),
};
Expand Down
104 changes: 78 additions & 26 deletions crates/uv/src/commands/project/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::fmt::Write;

use anyhow::Result;
use anyhow::{Context, Result};
use itertools::Itertools;
use owo_colors::OwoColorize;
use tracing::debug;
Expand All @@ -21,8 +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, Toolchain};
use uv_toolchain::{PythonEnvironment, SystemPython, Toolchain, ToolchainRequest};
use uv_types::{BuildIsolation, HashStrategy, InFlight};
use uv_warnings::warn_user;

use crate::commands::pip;
use crate::printer::Printer;
Expand Down Expand Up @@ -81,42 +82,93 @@ pub(crate) fn find_environment(
/// Initialize a virtual environment for the current project.
pub(crate) fn init_environment(
workspace: &Workspace,
python: Option<&str>,
preview: PreviewMode,
cache: &Cache,
printer: Printer,
) -> Result<PythonEnvironment, ProjectError> {
// Discover or create the virtual environment.
// TODO(charlie): If the environment isn't compatible with `--python`, recreate it.
match find_environment(workspace, cache) {
Ok(venv) => Ok(venv),
Err(uv_toolchain::Error::NotFound(_)) => {
// TODO(charlie): Respect `--python`; if unset, respect `Requires-Python`.
let interpreter = Toolchain::find_default(preview, cache)?.into_interpreter();
let venv = workspace.root().join(".venv");

writeln!(
printer.stderr(),
"Using Python {} interpreter at: {}",
interpreter.python_version(),
interpreter.sys_executable().user_display().cyan()
)?;
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) {
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);
}

let venv = workspace.venv();
writeln!(
printer.stderr(),
"Creating virtualenv at: {}",
venv.user_display().cyan()
"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()),
}

Ok(uv_virtualenv::create_venv(
&venv,
interpreter,
uv_virtualenv::Prompt::None,
false,
false,
)?)
// TODO(konsti): Extend `VersionRequest` to support `VersionSpecifiers`.
let requires_python_str = requires_python.map(ToString::to_string);
let interpreter = Toolchain::find(
python.or(requires_python_str.as_deref()),
// Otherwise we'll try to use the venv we just deleted.
SystemPython::Required,
preview,
cache,
)?
.into_interpreter();

if let Some(requires_python) = requires_python {
if !requires_python.contains(interpreter.python_version()) {
warn_user!(
"The Python {} you requested with {} is incompatible with the requirement of the \
project of {}",
interpreter.python_version(),
python.unwrap_or("(default)"),
requires_python
);
}
Err(e) => Err(e.into()),
}

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

let venv = workspace.venv();
writeln!(
printer.stderr(),
"Creating virtualenv at: {}",
venv.user_display().cyan()
)?;

Ok(uv_virtualenv::create_venv(
&venv,
interpreter,
uv_virtualenv::Prompt::None,
false,
false,
)?)
}

/// Update a [`PythonEnvironment`] to satisfy a set of [`RequirementsSource`]s.
Expand Down
8 changes: 7 additions & 1 deletion crates/uv/src/commands/project/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,13 @@ pub(crate) async fn run(
} else {
ProjectWorkspace::discover(&std::env::current_dir()?, None).await?
};
let venv = project::init_environment(project.workspace(), preview, cache, printer)?;
let venv = project::init_environment(
project.workspace(),
python.as_deref(),
preview,
cache,
printer,
)?;

// Lock and sync the environment.
let root_project_name = project
Expand Down
9 changes: 8 additions & 1 deletion crates/uv/src/commands/project/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ pub(crate) async fn sync(
index_locations: IndexLocations,
extras: ExtrasSpecification,
dev: bool,
python: Option<String>,
preview: PreviewMode,
cache: &Cache,
printer: Printer,
Expand All @@ -40,7 +41,13 @@ 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(), preview, cache, printer)?;
let venv = project::init_environment(
project.workspace(),
python.as_deref(),
preview,
cache,
printer,
)?;

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

0 comments on commit 90947a9

Please sign in to comment.