Error when discovered Python is incompatible with --isolated workspace (#6885)
## Summary We should have consistent errors with and without `--isolated`.
This commit is contained in:
@@ -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,
|
||||
@@ -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))
|
||||
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
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?;
|
||||
// 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?;
|
||||
|
||||
PythonInstallation::find_or_download(
|
||||
python_request.as_ref(),
|
||||
EnvironmentPreference::Any,
|
||||
python_preference,
|
||||
python_downloads,
|
||||
&client_builder,
|
||||
cache,
|
||||
Some(&download_reporter),
|
||||
)
|
||||
.await?
|
||||
.into_interpreter()
|
||||
};
|
||||
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()?;
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user