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

Warn when discovered Python is incompatible with PEP 723 script #6884

Merged
merged 1 commit into from
Aug 30, 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
52 changes: 33 additions & 19 deletions crates/uv/src/commands/project/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -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()
Expand All @@ -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.
Expand Down Expand Up @@ -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"
);
}
Expand Down Expand Up @@ -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");
}
}

Expand Down
62 changes: 62 additions & 0 deletions crates/uv/tests/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
Loading