Skip to content

Commit

Permalink
Error when discovered Python is incompatible with --isolated workspace
Browse files Browse the repository at this point in the history
  • Loading branch information
charliermarsh committed Aug 30, 2024
1 parent f21f1f2 commit bca085d
Show file tree
Hide file tree
Showing 3 changed files with 125 additions and 54 deletions.
70 changes: 41 additions & 29 deletions crates/uv/src/commands/project/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,46 @@ pub(crate) fn find_requires_python(
}))
}

/// Returns an error if the [`Interpreter`] does not satisfy the [`Workspace`] `requires-python`.
#[allow(clippy::result_large_err)]
pub(crate) fn validate_requires_python(
interpreter: &Interpreter,
workspace: &Workspace,
requires_python: &RequiresPython,
) -> Result<(), ProjectError> {
if !requires_python.contains(interpreter.python_version()) {
// If the Python version is compatible with one of the workspace _members_, raise
// a dedicated error. For example, if the workspace root requires Python >=3.12, but
// a library in the workspace is compatible with Python >=3.8, the user may attempt
// to sync on Python 3.8. This will fail, but we should provide a more helpful error
// message.
for (name, member) in workspace.packages() {
let Some(project) = member.pyproject_toml().project.as_ref() else {
continue;
};
let Some(specifiers) = project.requires_python.as_ref() else {
continue;
};
if specifiers.contains(interpreter.python_version()) {
return Err(ProjectError::RequestedMemberPythonIncompatibility(
interpreter.python_version().clone(),
requires_python.clone(),
name.clone(),
specifiers.clone(),
member.root().clone(),
));
}
}

return Err(ProjectError::RequestedPythonIncompatibility(
interpreter.python_version().clone(),
requires_python.clone(),
));
}

Ok(())
}

/// Find the virtual environment for the current project.
fn find_environment(
workspace: &Workspace,
Expand Down Expand Up @@ -297,35 +337,7 @@ impl FoundInterpreter {
}

if let Some(requires_python) = requires_python.as_ref() {
if !requires_python.contains(interpreter.python_version()) {
// If the Python version is compatible with one of the workspace _members_, raise
// a dedicated error. For example, if the workspace root requires Python >=3.12, but
// a library in the workspace is compatible with Python >=3.8, the user may attempt
// to sync on Python 3.8. This will fail, but we should provide a more helpful error
// message.
for (name, member) in workspace.packages() {
let Some(project) = member.pyproject_toml().project.as_ref() else {
continue;
};
let Some(specifiers) = project.requires_python.as_ref() else {
continue;
};
if specifiers.contains(interpreter.python_version()) {
return Err(ProjectError::RequestedMemberPythonIncompatibility(
interpreter.python_version().clone(),
requires_python.clone(),
name.clone(),
specifiers.clone(),
member.root().clone(),
));
}
}

return Err(ProjectError::RequestedPythonIncompatibility(
interpreter.python_version().clone(),
requires_python.clone(),
));
}
validate_requires_python(&interpreter, workspace, requires_python)?;
}

Ok(Self::Interpreter(interpreter))
Expand Down
55 changes: 30 additions & 25 deletions crates/uv/src/commands/project/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ use crate::commands::pip::loggers::{
use crate::commands::pip::operations;
use crate::commands::pip::operations::Modifications;
use crate::commands::project::environment::CachedEnvironment;
use crate::commands::project::{ProjectError, WorkspacePython};
use crate::commands::project::{validate_requires_python, ProjectError, WorkspacePython};
use crate::commands::reporters::PythonDownloadReporter;
use crate::commands::{project, ExitStatus, SharedState};
use crate::printer::Printer;
Expand Down Expand Up @@ -352,30 +352,35 @@ pub(crate) async fn run(

// If we're isolating the environment, use an ephemeral virtual environment as the
// base environment for the project.
let interpreter = {
let client_builder = BaseClientBuilder::new()
.connectivity(connectivity)
.native_tls(native_tls);

// Resolve the Python request and requirement for the workspace.
let WorkspacePython { python_request, .. } = WorkspacePython::from_request(
python.as_deref().map(PythonRequest::parse),
project.workspace(),
)
.await?;

PythonInstallation::find_or_download(
python_request.as_ref(),
EnvironmentPreference::Any,
python_preference,
python_downloads,
&client_builder,
cache,
Some(&download_reporter),
)
.await?
.into_interpreter()
};
let client_builder = BaseClientBuilder::new()
.connectivity(connectivity)
.native_tls(native_tls);

// Resolve the Python request and requirement for the workspace.
let WorkspacePython {
python_request,
requires_python,
} = WorkspacePython::from_request(
python.as_deref().map(PythonRequest::parse),
project.workspace(),
)
.await?;

let interpreter = PythonInstallation::find_or_download(
python_request.as_ref(),
EnvironmentPreference::Any,
python_preference,
python_downloads,
&client_builder,
cache,
Some(&download_reporter),
)
.await?
.into_interpreter();

if let Some(requires_python) = requires_python.as_ref() {
validate_requires_python(&interpreter, project.workspace(), requires_python)?;
}

// Create a virtual environment
temp_dir = cache.environment()?;
Expand Down
54 changes: 54 additions & 0 deletions crates/uv/tests/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1617,3 +1617,57 @@ fn run_project_toml_error() -> Result<()> {

Ok(())
}

#[test]
fn run_isolated_incompatible_python() -> Result<()> {
let context = TestContext::new_with_versions(&["3.8", "3.11"]);

let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(indoc! { r#"
[project]
name = "foo"
version = "1.0.0"
requires-python = ">=3.12"
dependencies = ["iniconfig"]
[build-system]
requires = ["setuptools>=42", "wheel"]
build-backend = "setuptools.build_meta"
"#
})?;

let python_version = context.temp_dir.child(PYTHON_VERSION_FILENAME);
python_version.write_str("3.8")?;

let test_script = context.temp_dir.child("main.py");
test_script.write_str(indoc! { r#"
import iniconfig
x: str | int = "hello"
print(x)
"#
})?;

// We should reject Python 3.8...
uv_snapshot!(context.filters(), context.run().arg("main.py"), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
Using Python 3.8.[X] interpreter at: [PYTHON-3.8]
error: The requested Python interpreter (3.8.[X]) is incompatible with the project Python requirement: `>=3.12`
"###);

// ...even if `--isolated` is provided.
uv_snapshot!(context.filters(), context.run().arg("--isolated").arg("main.py"), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: The requested Python interpreter (3.8.[X]) is incompatible with the project Python requirement: `>=3.12`
"###);

Ok(())
}

0 comments on commit bca085d

Please sign in to comment.