Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Clarify Python requirement source for script incompatibilities #7339

Merged
merged 1 commit into from
Sep 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading