Skip to content

Commit

Permalink
Clarify Python requirement source for script incompatibilities (#7339)
Browse files Browse the repository at this point in the history
## Summary

Closes #7293.
  • Loading branch information
charliermarsh authored Sep 12, 2024
1 parent dcde459 commit 3d2b94f
Show file tree
Hide file tree
Showing 4 changed files with 80 additions and 45 deletions.
61 changes: 37 additions & 24 deletions crates/uv/src/commands/project/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,22 @@ pub(crate) enum ProjectError {
LockedPlatformIncompatibility(String),

#[error("The requested interpreter resolved to Python {0}, which is incompatible with the project's Python requirement: `{1}`")]
RequestedPythonIncompatibility(Version, RequiresPython),
RequestedPythonProjectIncompatibility(Version, RequiresPython),

#[error("The Python request from `{0}` resolved to Python {1}, which incompatible with the project's Python requirement: `{2}`")]
DotPythonVersionPythonIncompatibility(String, Version, RequiresPython),
#[error("The Python request from `{0}` resolved to Python {1}, which is incompatible with the project's Python requirement: `{2}`")]
DotPythonVersionProjectIncompatibility(String, Version, RequiresPython),

#[error("The resolved Python interpreter (Python {0}) is incompatible with the project's Python requirement: `{1}`")]
RequiresPythonIncompatibility(Version, RequiresPython),
RequiresPythonProjectIncompatibility(Version, RequiresPython),

#[error("The requested interpreter resolved to Python {0}, which is incompatible with the script's Python requirement: `{1}`")]
RequestedPythonScriptIncompatibility(Version, VersionSpecifiers),

#[error("The Python request from `{0}` resolved to Python {1}, which is incompatible with the script's Python requirement: `{2}`")]
DotPythonVersionScriptIncompatibility(String, Version, VersionSpecifiers),

#[error("The resolved Python interpreter (Python {0}) is incompatible with the script's Python requirement: `{1}`")]
RequiresPythonScriptIncompatibility(Version, VersionSpecifiers),

#[error("The requested interpreter resolved to Python {0}, which is incompatible with the project's Python requirement: `{1}`. However, a workspace member (`{member}`) supports Python {3}. To install the workspace member on its own, navigate to `{path}`, then run `{venv}` followed by `{install}`.", member = _2.cyan(), venv = format!("uv venv --python {_0}").green(), install = "uv pip install -e .".green(), path = _4.user_display().cyan() )]
RequestedMemberIncompatibility(
Expand All @@ -81,7 +90,7 @@ pub(crate) enum ProjectError {
PathBuf,
),

#[error("The Python request from `{0}` resolved to Python {1}, which incompatible with the project's Python requirement: `{2}`. However, a workspace member (`{member}`) supports Python {4}. To install the workspace member on its own, navigate to `{path}`, then run `{venv}` followed by `{install}`.", member = _3.cyan(), venv = format!("uv venv --python {_1}").green(), install = "uv pip install -e .".green(), path = _5.user_display().cyan() )]
#[error("The Python request from `{0}` resolved to Python {1}, which is incompatible with the project's Python requirement: `{2}`. However, a workspace member (`{member}`) supports Python {4}. To install the workspace member on its own, navigate to `{path}`, then run `{venv}` followed by `{install}`.", member = _3.cyan(), venv = format!("uv venv --python {_1}").green(), install = "uv pip install -e .".green(), path = _5.user_display().cyan() )]
DotPythonVersionMemberIncompatibility(
String,
Version,
Expand Down Expand Up @@ -186,7 +195,7 @@ pub(crate) fn validate_requires_python(
interpreter: &Interpreter,
workspace: &Workspace,
requires_python: &RequiresPython,
source: &WorkspacePythonSource,
source: &PythonRequestSource,
) -> Result<(), ProjectError> {
if requires_python.contains(interpreter.python_version()) {
return Ok(());
Expand All @@ -206,7 +215,7 @@ pub(crate) fn validate_requires_python(
};
if specifiers.contains(interpreter.python_version()) {
return match source {
WorkspacePythonSource::UserRequest => {
PythonRequestSource::UserRequest => {
Err(ProjectError::RequestedMemberIncompatibility(
interpreter.python_version().clone(),
requires_python.clone(),
Expand All @@ -215,7 +224,7 @@ pub(crate) fn validate_requires_python(
member.root().clone(),
))
}
WorkspacePythonSource::DotPythonVersion(file) => {
PythonRequestSource::DotPythonVersion(file) => {
Err(ProjectError::DotPythonVersionMemberIncompatibility(
file.to_string(),
interpreter.python_version().clone(),
Expand All @@ -225,7 +234,7 @@ pub(crate) fn validate_requires_python(
member.root().clone(),
))
}
WorkspacePythonSource::RequiresPython => {
PythonRequestSource::RequiresPython => {
Err(ProjectError::RequiresPythonMemberIncompatibility(
interpreter.python_version().clone(),
requires_python.clone(),
Expand All @@ -239,21 +248,25 @@ pub(crate) fn validate_requires_python(
}

match source {
WorkspacePythonSource::UserRequest => Err(ProjectError::RequestedPythonIncompatibility(
interpreter.python_version().clone(),
requires_python.clone(),
)),
WorkspacePythonSource::DotPythonVersion(file) => {
Err(ProjectError::DotPythonVersionPythonIncompatibility(
PythonRequestSource::UserRequest => {
Err(ProjectError::RequestedPythonProjectIncompatibility(
interpreter.python_version().clone(),
requires_python.clone(),
))
}
PythonRequestSource::DotPythonVersion(file) => {
Err(ProjectError::DotPythonVersionProjectIncompatibility(
file.to_string(),
interpreter.python_version().clone(),
requires_python.clone(),
))
}
WorkspacePythonSource::RequiresPython => Err(ProjectError::RequiresPythonIncompatibility(
interpreter.python_version().clone(),
requires_python.clone(),
)),
PythonRequestSource::RequiresPython => {
Err(ProjectError::RequiresPythonProjectIncompatibility(
interpreter.python_version().clone(),
requires_python.clone(),
))
}
}
}

Expand All @@ -273,7 +286,7 @@ pub(crate) enum FoundInterpreter {
}

#[derive(Debug, Clone)]
pub(crate) enum WorkspacePythonSource {
pub(crate) enum PythonRequestSource {
/// The request was provided by the user.
UserRequest,
/// The request was inferred from a `.python-version` or `.python-versions` file.
Expand All @@ -286,7 +299,7 @@ pub(crate) enum WorkspacePythonSource {
#[derive(Debug, Clone)]
pub(crate) struct WorkspacePython {
/// The source of the Python request.
source: WorkspacePythonSource,
source: PythonRequestSource,
/// The resolved Python request, computed by considering (1) any explicit request from the user
/// via `--python`, (2) any implicit request from the user via `.python-version`, and (3) any
/// `Requires-Python` specifier in the `pyproject.toml`.
Expand All @@ -306,14 +319,14 @@ impl WorkspacePython {

let (source, python_request) = if let Some(request) = python_request {
// (1) Explicit request from user
let source = WorkspacePythonSource::UserRequest;
let source = PythonRequestSource::UserRequest;
let request = Some(request);
(source, request)
} else if let Some(file) =
PythonVersionFile::discover(workspace.install_path(), false, false).await?
{
// (2) Request from `.python-version`
let source = WorkspacePythonSource::DotPythonVersion(file.file_name().to_string());
let source = PythonRequestSource::DotPythonVersion(file.file_name().to_string());
let request = file.into_version();
(source, request)
} else {
Expand All @@ -324,7 +337,7 @@ impl WorkspacePython {
.map(|specifiers| {
PythonRequest::Version(VersionRequest::Range(specifiers.clone()))
});
let source = WorkspacePythonSource::RequiresPython;
let source = PythonRequestSource::RequiresPython;
(source, request)
};

Expand Down
56 changes: 39 additions & 17 deletions crates/uv/src/commands/project/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ 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::{validate_requires_python, ProjectError, WorkspacePython};
use crate::commands::project::{
validate_requires_python, ProjectError, PythonRequestSource, WorkspacePython,
};
use crate::commands::reporters::PythonDownloadReporter;
use crate::commands::{project, ExitStatus, SharedState};
use crate::printer::Printer;
Expand Down Expand Up @@ -103,24 +105,27 @@ pub(crate) async fn run(
script.path.user_display().cyan()
)?;

// (1) Explicit request from user
let python_request = if let Some(request) = python.as_deref() {
Some(PythonRequest::parse(request))
let (source, python_request) = if let Some(request) = python.as_deref() {
// (1) Explicit request from user
let source = PythonRequestSource::UserRequest;
let request = Some(PythonRequest::parse(request));
(source, request)
} else if let Some(file) = PythonVersionFile::discover(&*CWD, false, false).await? {
// (2) Request from `.python-version`
} else if let Some(request) = PythonVersionFile::discover(&*CWD, false, false)
.await?
.and_then(PythonVersionFile::into_version)
{
Some(request)
// (3) `Requires-Python` in the script
let source = PythonRequestSource::DotPythonVersion(file.file_name().to_string());
let request = file.into_version();
(source, request)
} else {
script
// (3) `Requires-Python` in the script
let request = script
.metadata
.requires_python
.as_ref()
.map(|requires_python| {
PythonRequest::Version(VersionRequest::Range(requires_python.clone()))
})
});
let source = PythonRequestSource::RequiresPython;
(source, request)
};

let client_builder = BaseClientBuilder::new()
Expand All @@ -141,11 +146,28 @@ pub(crate) async fn run(

if let Some(requires_python) = script.metadata.requires_python.as_ref() {
if !requires_python.contains(interpreter.python_version()) {
warn_user!(
"Python {} does not satisfy the script's `requires-python` specifier: `{}`",
interpreter.python_version(),
requires_python
);
let err = match source {
PythonRequestSource::UserRequest => {
ProjectError::RequestedPythonScriptIncompatibility(
interpreter.python_version().clone(),
requires_python.clone(),
)
}
PythonRequestSource::DotPythonVersion(file) => {
ProjectError::DotPythonVersionScriptIncompatibility(
file,
interpreter.python_version().clone(),
requires_python.clone(),
)
}
PythonRequestSource::RequiresPython => {
ProjectError::RequiresPythonScriptIncompatibility(
interpreter.python_version().clone(),
requires_python.clone(),
)
}
};
warn_user!("{err}");
}
}

Expand Down
2 changes: 1 addition & 1 deletion crates/uv/tests/lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12623,7 +12623,7 @@ fn lock_request_requires_python() -> Result<()> {

----- stderr -----
Using Python 3.12.[X] interpreter at: [PYTHON-3.12]
error: The Python request from `.python-version` resolved to Python 3.12.[X], which incompatible with the project's Python requirement: `>=3.8, <=3.10`
error: The Python request from `.python-version` resolved to Python 3.12.[X], which is incompatible with the project's Python requirement: `>=3.8, <=3.10`
"###);

Ok(())
Expand Down
6 changes: 3 additions & 3 deletions crates/uv/tests/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -418,7 +418,7 @@ fn run_pep723_script_requires_python() -> Result<()> {
----- stderr -----
Reading inline script metadata from: main.py
warning: Python 3.8.[X] does not satisfy the script's `requires-python` specifier: `>=3.11`
warning: The Python request from `.python-version` resolved to Python 3.8.[X], which is incompatible with the script's Python requirement: `>=3.11`
Resolved 1 package in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
Expand Down Expand Up @@ -1774,7 +1774,7 @@ fn run_isolated_incompatible_python() -> Result<()> {
----- stderr -----
Using Python 3.8.[X] interpreter at: [PYTHON-3.8]
error: The Python request from `.python-version` resolved to Python 3.8.[X], which incompatible with the project's Python requirement: `>=3.12`
error: The Python request from `.python-version` resolved to Python 3.8.[X], which is incompatible with the project's Python requirement: `>=3.12`
"###);

// ...even if `--isolated` is provided.
Expand All @@ -1784,7 +1784,7 @@ fn run_isolated_incompatible_python() -> Result<()> {
----- stdout -----
----- stderr -----
error: The Python request from `.python-version` resolved to Python 3.8.[X], which incompatible with the project's Python requirement: `>=3.12`
error: The Python request from `.python-version` resolved to Python 3.8.[X], which is incompatible with the project's Python requirement: `>=3.12`
"###);

Ok(())
Expand Down

0 comments on commit 3d2b94f

Please sign in to comment.