Skip to content

Commit

Permalink
Warn when discovered Python is incompatible with PEP 723 script
Browse files Browse the repository at this point in the history
  • Loading branch information
charliermarsh committed Aug 30, 2024
1 parent fa14983 commit 333ea83
Show file tree
Hide file tree
Showing 2 changed files with 95 additions and 19 deletions.
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

0 comments on commit 333ea83

Please sign in to comment.