diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index 21bf86acf4ac..ca8d800c38da 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -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( @@ -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, @@ -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(()); @@ -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(), @@ -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(), @@ -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(), @@ -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(), + )) + } } } @@ -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. @@ -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`. @@ -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 { @@ -324,7 +337,7 @@ impl WorkspacePython { .map(|specifiers| { PythonRequest::Version(VersionRequest::Range(specifiers.clone())) }); - let source = WorkspacePythonSource::RequiresPython; + let source = PythonRequestSource::RequiresPython; (source, request) }; diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index 8334db3955d3..253087c9680d 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -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; @@ -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() @@ -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}"); } } diff --git a/crates/uv/tests/lock.rs b/crates/uv/tests/lock.rs index 88c6254e07cd..aaebf89f7692 100644 --- a/crates/uv/tests/lock.rs +++ b/crates/uv/tests/lock.rs @@ -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(()) diff --git a/crates/uv/tests/run.rs b/crates/uv/tests/run.rs index 300b41309490..9d8a3078c1a1 100644 --- a/crates/uv/tests/run.rs +++ b/crates/uv/tests/run.rs @@ -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] @@ -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. @@ -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(())