Skip to content

Commit b7e271b

Browse files
committed
Clarify Python requirement source for script incompatibilities
1 parent d52af0c commit b7e271b

File tree

4 files changed

+80
-45
lines changed

4 files changed

+80
-45
lines changed

crates/uv/src/commands/project/mod.rs

Lines changed: 37 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -64,13 +64,22 @@ pub(crate) enum ProjectError {
6464
LockedPlatformIncompatibility(String),
6565

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

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

7272
#[error("The resolved Python interpreter (Python {0}) is incompatible with the project's Python requirement: `{1}`")]
73-
RequiresPythonIncompatibility(Version, RequiresPython),
73+
RequiresPythonProjectIncompatibility(Version, RequiresPython),
74+
75+
#[error("The requested interpreter resolved to Python {0}, which is incompatible with the script's Python requirement: `{1}`")]
76+
RequestedPythonScriptIncompatibility(Version, VersionSpecifiers),
77+
78+
#[error("The Python request from `{0}` resolved to Python {1}, which is incompatible with the script's Python requirement: `{2}`")]
79+
DotPythonVersionScriptIncompatibility(String, Version, VersionSpecifiers),
80+
81+
#[error("The resolved Python interpreter (Python {0}) is incompatible with the script's Python requirement: `{1}`")]
82+
RequiresPythonScriptIncompatibility(Version, VersionSpecifiers),
7483

7584
#[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() )]
7685
RequestedMemberIncompatibility(
@@ -81,7 +90,7 @@ pub(crate) enum ProjectError {
8190
PathBuf,
8291
),
8392

84-
#[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() )]
93+
#[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() )]
8594
DotPythonVersionMemberIncompatibility(
8695
String,
8796
Version,
@@ -186,7 +195,7 @@ pub(crate) fn validate_requires_python(
186195
interpreter: &Interpreter,
187196
workspace: &Workspace,
188197
requires_python: &RequiresPython,
189-
source: &WorkspacePythonSource,
198+
source: &PythonRequestSource,
190199
) -> Result<(), ProjectError> {
191200
if requires_python.contains(interpreter.python_version()) {
192201
return Ok(());
@@ -206,7 +215,7 @@ pub(crate) fn validate_requires_python(
206215
};
207216
if specifiers.contains(interpreter.python_version()) {
208217
return match source {
209-
WorkspacePythonSource::UserRequest => {
218+
PythonRequestSource::UserRequest => {
210219
Err(ProjectError::RequestedMemberIncompatibility(
211220
interpreter.python_version().clone(),
212221
requires_python.clone(),
@@ -215,7 +224,7 @@ pub(crate) fn validate_requires_python(
215224
member.root().clone(),
216225
))
217226
}
218-
WorkspacePythonSource::DotPythonVersion(file) => {
227+
PythonRequestSource::DotPythonVersion(file) => {
219228
Err(ProjectError::DotPythonVersionMemberIncompatibility(
220229
file.to_string(),
221230
interpreter.python_version().clone(),
@@ -225,7 +234,7 @@ pub(crate) fn validate_requires_python(
225234
member.root().clone(),
226235
))
227236
}
228-
WorkspacePythonSource::RequiresPython => {
237+
PythonRequestSource::RequiresPython => {
229238
Err(ProjectError::RequiresPythonMemberIncompatibility(
230239
interpreter.python_version().clone(),
231240
requires_python.clone(),
@@ -239,21 +248,25 @@ pub(crate) fn validate_requires_python(
239248
}
240249

241250
match source {
242-
WorkspacePythonSource::UserRequest => Err(ProjectError::RequestedPythonIncompatibility(
243-
interpreter.python_version().clone(),
244-
requires_python.clone(),
245-
)),
246-
WorkspacePythonSource::DotPythonVersion(file) => {
247-
Err(ProjectError::DotPythonVersionPythonIncompatibility(
251+
PythonRequestSource::UserRequest => {
252+
Err(ProjectError::RequestedPythonProjectIncompatibility(
253+
interpreter.python_version().clone(),
254+
requires_python.clone(),
255+
))
256+
}
257+
PythonRequestSource::DotPythonVersion(file) => {
258+
Err(ProjectError::DotPythonVersionProjectIncompatibility(
248259
file.to_string(),
249260
interpreter.python_version().clone(),
250261
requires_python.clone(),
251262
))
252263
}
253-
WorkspacePythonSource::RequiresPython => Err(ProjectError::RequiresPythonIncompatibility(
254-
interpreter.python_version().clone(),
255-
requires_python.clone(),
256-
)),
264+
PythonRequestSource::RequiresPython => {
265+
Err(ProjectError::RequiresPythonProjectIncompatibility(
266+
interpreter.python_version().clone(),
267+
requires_python.clone(),
268+
))
269+
}
257270
}
258271
}
259272

@@ -273,7 +286,7 @@ pub(crate) enum FoundInterpreter {
273286
}
274287

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

307320
let (source, python_request) = if let Some(request) = python_request {
308321
// (1) Explicit request from user
309-
let source = WorkspacePythonSource::UserRequest;
322+
let source = PythonRequestSource::UserRequest;
310323
let request = Some(request);
311324
(source, request)
312325
} else if let Some(file) =
313326
PythonVersionFile::discover(workspace.install_path(), false, false).await?
314327
{
315328
// (2) Request from `.python-version`
316-
let source = WorkspacePythonSource::DotPythonVersion(file.file_name().to_string());
329+
let source = PythonRequestSource::DotPythonVersion(file.file_name().to_string());
317330
let request = file.into_version();
318331
(source, request)
319332
} else {
@@ -324,7 +337,7 @@ impl WorkspacePython {
324337
.map(|specifiers| {
325338
PythonRequest::Version(VersionRequest::Range(specifiers.clone()))
326339
});
327-
let source = WorkspacePythonSource::RequiresPython;
340+
let source = PythonRequestSource::RequiresPython;
328341
(source, request)
329342
};
330343

crates/uv/src/commands/project/run.rs

Lines changed: 39 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@ use crate::commands::pip::loggers::{
3434
use crate::commands::pip::operations;
3535
use crate::commands::pip::operations::Modifications;
3636
use crate::commands::project::environment::CachedEnvironment;
37-
use crate::commands::project::{validate_requires_python, ProjectError, WorkspacePython};
37+
use crate::commands::project::{
38+
validate_requires_python, ProjectError, PythonRequestSource, WorkspacePython,
39+
};
3840
use crate::commands::reporters::PythonDownloadReporter;
3941
use crate::commands::{project, ExitStatus, SharedState};
4042
use crate::printer::Printer;
@@ -103,24 +105,27 @@ pub(crate) async fn run(
103105
script.path.user_display().cyan()
104106
)?;
105107

106-
// (1) Explicit request from user
107-
let python_request = if let Some(request) = python.as_deref() {
108-
Some(PythonRequest::parse(request))
108+
let (source, python_request) = if let Some(request) = python.as_deref() {
109+
// (1) Explicit request from user
110+
let source = PythonRequestSource::UserRequest;
111+
let request = Some(PythonRequest::parse(request));
112+
(source, request)
113+
} else if let Some(file) = PythonVersionFile::discover(&*CWD, false, false).await? {
109114
// (2) Request from `.python-version`
110-
} else if let Some(request) = PythonVersionFile::discover(&*CWD, false, false)
111-
.await?
112-
.and_then(PythonVersionFile::into_version)
113-
{
114-
Some(request)
115-
// (3) `Requires-Python` in the script
115+
let source = PythonRequestSource::DotPythonVersion(file.file_name().to_string());
116+
let request = file.into_version();
117+
(source, request)
116118
} else {
117-
script
119+
// (3) `Requires-Python` in the script
120+
let request = script
118121
.metadata
119122
.requires_python
120123
.as_ref()
121124
.map(|requires_python| {
122125
PythonRequest::Version(VersionRequest::Range(requires_python.clone()))
123-
})
126+
});
127+
let source = PythonRequestSource::RequiresPython;
128+
(source, request)
124129
};
125130

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

142147
if let Some(requires_python) = script.metadata.requires_python.as_ref() {
143148
if !requires_python.contains(interpreter.python_version()) {
144-
warn_user!(
145-
"Python {} does not satisfy the script's `requires-python` specifier: `{}`",
146-
interpreter.python_version(),
147-
requires_python
148-
);
149+
let err = match source {
150+
PythonRequestSource::UserRequest => {
151+
ProjectError::RequestedPythonScriptIncompatibility(
152+
interpreter.python_version().clone(),
153+
requires_python.clone(),
154+
)
155+
}
156+
PythonRequestSource::DotPythonVersion(file) => {
157+
ProjectError::DotPythonVersionScriptIncompatibility(
158+
file,
159+
interpreter.python_version().clone(),
160+
requires_python.clone(),
161+
)
162+
}
163+
PythonRequestSource::RequiresPython => {
164+
ProjectError::RequiresPythonScriptIncompatibility(
165+
interpreter.python_version().clone(),
166+
requires_python.clone(),
167+
)
168+
}
169+
};
170+
warn_user!("{err}");
149171
}
150172
}
151173

crates/uv/tests/lock.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12623,7 +12623,7 @@ fn lock_request_requires_python() -> Result<()> {
1262312623

1262412624
----- stderr -----
1262512625
Using Python 3.12.[X] interpreter at: [PYTHON-3.12]
12626-
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`
12626+
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`
1262712627
"###);
1262812628

1262912629
Ok(())

crates/uv/tests/run.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -418,7 +418,7 @@ fn run_pep723_script_requires_python() -> Result<()> {
418418
419419
----- stderr -----
420420
Reading inline script metadata from: main.py
421-
warning: Python 3.8.[X] does not satisfy the script's `requires-python` specifier: `>=3.11`
421+
warning: The Python request from `.python-version` resolved to Python 3.8.[X], which is incompatible with the script's Python requirement: `>=3.11`
422422
Resolved 1 package in [TIME]
423423
Prepared 1 package in [TIME]
424424
Installed 1 package in [TIME]
@@ -1774,7 +1774,7 @@ fn run_isolated_incompatible_python() -> Result<()> {
17741774
17751775
----- stderr -----
17761776
Using Python 3.8.[X] interpreter at: [PYTHON-3.8]
1777-
error: The Python request from `.python-version` resolved to Python 3.8.[X], which incompatible with the project's Python requirement: `>=3.12`
1777+
error: The Python request from `.python-version` resolved to Python 3.8.[X], which is incompatible with the project's Python requirement: `>=3.12`
17781778
"###);
17791779

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

17901790
Ok(())

0 commit comments

Comments
 (0)