Warn when discovered Python is incompatible with PEP 723 script (#6884)
## Summary Closes https://github.com/astral-sh/uv/issues/6883.
This commit is contained in:
@@ -25,7 +25,7 @@ use uv_python::{
|
||||
};
|
||||
use uv_requirements::{RequirementsSource, RequirementsSpecification};
|
||||
use uv_scripts::Pep723Script;
|
||||
use uv_warnings::warn_user_once;
|
||||
use uv_warnings::warn_user;
|
||||
use uv_workspace::{DiscoveryOptions, VirtualProject, Workspace, WorkspaceError};
|
||||
|
||||
use crate::commands::pip::loggers::{
|
||||
@@ -111,11 +111,15 @@ pub(crate) async fn run(
|
||||
.and_then(PythonVersionFile::into_version)
|
||||
{
|
||||
Some(request)
|
||||
// (3) `Requires-Python` in `pyproject.toml`
|
||||
// (3) `Requires-Python` in the script
|
||||
} else {
|
||||
script.metadata.requires_python.map(|requires_python| {
|
||||
PythonRequest::Version(VersionRequest::Range(requires_python))
|
||||
})
|
||||
script
|
||||
.metadata
|
||||
.requires_python
|
||||
.as_ref()
|
||||
.map(|requires_python| {
|
||||
PythonRequest::Version(VersionRequest::Range(requires_python.clone()))
|
||||
})
|
||||
};
|
||||
|
||||
let client_builder = BaseClientBuilder::new()
|
||||
@@ -134,6 +138,16 @@ pub(crate) async fn run(
|
||||
.await?
|
||||
.into_interpreter();
|
||||
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Install the script requirements, if necessary. Otherwise, use an isolated environment.
|
||||
if let Some(dependencies) = script.metadata.dependencies {
|
||||
// // Collect any `tool.uv.sources` from the script.
|
||||
@@ -224,28 +238,28 @@ pub(crate) async fn run(
|
||||
);
|
||||
}
|
||||
if !extras.is_empty() {
|
||||
warn_user_once!("Extras are not supported for Python scripts with inline metadata");
|
||||
warn_user!("Extras are not supported for Python scripts with inline metadata");
|
||||
}
|
||||
if !dev {
|
||||
warn_user_once!("`--no-dev` is not supported for Python scripts with inline metadata");
|
||||
warn_user!("`--no-dev` is not supported for Python scripts with inline metadata");
|
||||
}
|
||||
if package.is_some() {
|
||||
warn_user_once!(
|
||||
warn_user!(
|
||||
"`--package` is a no-op for Python scripts with inline metadata, which always run in isolation"
|
||||
);
|
||||
}
|
||||
if locked {
|
||||
warn_user_once!(
|
||||
warn_user!(
|
||||
"`--locked` is a no-op for Python scripts with inline metadata, which always run in isolation"
|
||||
);
|
||||
}
|
||||
if frozen {
|
||||
warn_user_once!(
|
||||
warn_user!(
|
||||
"`--frozen` is a no-op for Python scripts with inline metadata, which always run in isolation"
|
||||
);
|
||||
}
|
||||
if isolated {
|
||||
warn_user_once!(
|
||||
warn_user!(
|
||||
"`--isolated` is a no-op for Python scripts with inline metadata, which always run in isolation"
|
||||
);
|
||||
}
|
||||
@@ -293,30 +307,30 @@ pub(crate) async fn run(
|
||||
if no_project {
|
||||
// If the user ran with `--no-project` and provided a project-only setting, warn.
|
||||
if !extras.is_empty() {
|
||||
warn_user_once!("Extras have no effect when used alongside `--no-project`");
|
||||
warn_user!("Extras have no effect when used alongside `--no-project`");
|
||||
}
|
||||
if !dev {
|
||||
warn_user_once!("`--no-dev` has no effect when used alongside `--no-project`");
|
||||
warn_user!("`--no-dev` has no effect when used alongside `--no-project`");
|
||||
}
|
||||
if locked {
|
||||
warn_user_once!("`--locked` has no effect when used alongside `--no-project`");
|
||||
warn_user!("`--locked` has no effect when used alongside `--no-project`");
|
||||
}
|
||||
if frozen {
|
||||
warn_user_once!("`--frozen` has no effect when used alongside `--no-project`");
|
||||
warn_user!("`--frozen` has no effect when used alongside `--no-project`");
|
||||
}
|
||||
} else if project.is_none() {
|
||||
// If we can't find a project and the user provided a project-only setting, warn.
|
||||
if !extras.is_empty() {
|
||||
warn_user_once!("Extras have no effect when used outside of a project");
|
||||
warn_user!("Extras have no effect when used outside of a project");
|
||||
}
|
||||
if !dev {
|
||||
warn_user_once!("`--no-dev` has no effect when used outside of a project");
|
||||
warn_user!("`--no-dev` has no effect when used outside of a project");
|
||||
}
|
||||
if locked {
|
||||
warn_user_once!("`--locked` has no effect when used outside of a project");
|
||||
warn_user!("`--locked` has no effect when used outside of a project");
|
||||
}
|
||||
if frozen {
|
||||
warn_user_once!("`--frozen` has no effect when used outside of a project");
|
||||
warn_user!("`--frozen` has no effect when used outside of a project");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -386,6 +386,68 @@ fn run_pep723_script() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_pep723_script_requires_python() -> Result<()> {
|
||||
let context = TestContext::new_with_versions(&["3.8", "3.11"]);
|
||||
|
||||
// If we have a `.python-version` that's incompatible with the script, we should error.
|
||||
let python_version = context.temp_dir.child(PYTHON_VERSION_FILENAME);
|
||||
python_version.write_str("3.8")?;
|
||||
|
||||
// If the script contains a PEP 723 tag, we should install its requirements.
|
||||
let test_script = context.temp_dir.child("main.py");
|
||||
test_script.write_str(indoc! { r#"
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = [
|
||||
# "iniconfig",
|
||||
# ]
|
||||
# ///
|
||||
|
||||
import iniconfig
|
||||
|
||||
x: str | int = "hello"
|
||||
print(x)
|
||||
"#
|
||||
})?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.run().arg("main.py"), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Reading inline script metadata from: main.py
|
||||
warning: Python 3.8.[X] does not satisfy the script's `requires-python` specifier: `>=3.11`
|
||||
Resolved 1 package in [TIME]
|
||||
Prepared 1 package in [TIME]
|
||||
Installed 1 package in [TIME]
|
||||
+ iniconfig==2.0.0
|
||||
Traceback (most recent call last):
|
||||
File "main.py", line 10, in <module>
|
||||
x: str | int = "hello"
|
||||
TypeError: unsupported operand type(s) for |: 'type' and 'type'
|
||||
"###);
|
||||
|
||||
// Delete the `.python-version` file to allow the script to run.
|
||||
fs_err::remove_file(&python_version)?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.run().arg("main.py"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
hello
|
||||
|
||||
----- stderr -----
|
||||
Reading inline script metadata from: main.py
|
||||
Resolved 1 package in [TIME]
|
||||
Installed 1 package in [TIME]
|
||||
+ iniconfig==2.0.0
|
||||
"###);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Run a `.pyw` script. The script should be executed with `pythonw.exe`.
|
||||
#[test]
|
||||
#[cfg(windows)]
|
||||
|
||||
Reference in New Issue
Block a user