Error when `--script` is passing a non-PEP 723 script (#11118)

## Summary

We now show a custom error if (1) the file doesn't exist at all, or (2)
it's not a PEP 723 script.

In the future, `uv lock --script` should probably initialize the script,
but that requires a more extensive refactor. At present, we just
silently lock the project instead, which is pretty bad!

Closes https://github.com/astral-sh/uv/issues/10979.
This commit is contained in:
Charlie Marsh 2025-01-30 15:49:59 -05:00 committed by GitHub
parent e0a19be825
commit bf9fe1d36d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 94 additions and 43 deletions

View File

@ -121,13 +121,11 @@ pub struct Pep723Script {
impl Pep723Script {
/// Read the PEP 723 `script` metadata from a Python file, if it exists.
///
/// Returns `None` if the file is missing a PEP 723 metadata block.
///
/// See: <https://peps.python.org/pep-0723/>
pub async fn read(file: impl AsRef<Path>) -> Result<Option<Self>, Pep723Error> {
let contents = match fs_err::tokio::read(&file).await {
Ok(contents) => contents,
Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(None),
Err(err) => return Err(err.into()),
};
let contents = fs_err::tokio::read(&file).await?;
// Extract the `script` tag.
let ScriptTag {
@ -286,13 +284,11 @@ impl Pep723Metadata {
/// Read the PEP 723 `script` metadata from a Python file, if it exists.
///
/// Returns `None` if the file is missing a PEP 723 metadata block.
///
/// See: <https://peps.python.org/pep-0723/>
pub async fn read(file: impl AsRef<Path>) -> Result<Option<Self>, Pep723Error> {
let contents = match fs_err::tokio::read(&file).await {
Ok(contents) => contents,
Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(None),
Err(err) => return Err(err.into()),
};
let contents = fs_err::tokio::read(&file).await?;
// Extract the `script` tag.
let ScriptTag { metadata, .. } = match ScriptTag::parse(&contents) {
@ -341,6 +337,8 @@ pub struct ToolUv {
pub enum Pep723Error {
#[error("An opening tag (`# /// script`) was found without a closing tag (`# ///`). Ensure that every line between the opening and closing tags (including empty lines) starts with a leading `#`.")]
UnclosedBlock,
#[error("The PEP 723 metadata block is missing from the script.")]
MissingTag,
#[error(transparent)]
Io(#[from] io::Error),
#[error(transparent)]

View File

@ -25,9 +25,9 @@ use uv_cli::{
use uv_cli::{PythonCommand, PythonNamespace, ToolCommand, ToolNamespace, TopLevelArgs};
#[cfg(feature = "self-update")]
use uv_cli::{SelfCommand, SelfNamespace, SelfUpdateArgs};
use uv_fs::CWD;
use uv_fs::{Simplified, CWD};
use uv_requirements::RequirementsSource;
use uv_scripts::{Pep723Item, Pep723Metadata, Pep723Script};
use uv_scripts::{Pep723Error, Pep723Item, Pep723Metadata, Pep723Script};
use uv_settings::{Combine, FilesystemOptions, Options};
use uv_static::EnvVars;
use uv_warnings::{warn_user, warn_user_once};
@ -168,45 +168,67 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
// If the target is a PEP 723 script, parse it.
let script = if let Commands::Project(command) = &*cli.command {
if let ProjectCommand::Run(uv_cli::RunArgs { .. }) = &**command {
match run_command.as_ref() {
match &**command {
ProjectCommand::Run(uv_cli::RunArgs { .. }) => match run_command.as_ref() {
Some(
RunCommand::PythonScript(script, _) | RunCommand::PythonGuiScript(script, _),
) => Pep723Script::read(&script).await?.map(Pep723Item::Script),
) => match Pep723Script::read(&script).await {
Ok(Some(script)) => Some(Pep723Item::Script(script)),
Ok(None) => None,
Err(Pep723Error::Io(err)) if err.kind() == std::io::ErrorKind::NotFound => None,
Err(err) => return Err(err.into()),
},
Some(RunCommand::PythonRemote(script, _)) => {
Pep723Metadata::read(&script).await?.map(Pep723Item::Remote)
match Pep723Metadata::read(&script).await {
Ok(Some(metadata)) => Some(Pep723Item::Remote(metadata)),
Ok(None) => None,
Err(Pep723Error::Io(err)) if err.kind() == std::io::ErrorKind::NotFound => {
None
}
Err(err) => return Err(err.into()),
}
}
Some(
RunCommand::PythonStdin(contents, _) | RunCommand::PythonGuiStdin(contents, _),
) => Pep723Metadata::parse(contents)?.map(Pep723Item::Stdin),
_ => None,
}
} else if let ProjectCommand::Remove(uv_cli::RemoveArgs {
script: Some(script),
..
}) = &**command
{
Pep723Script::read(&script).await?.map(Pep723Item::Script)
} else if let ProjectCommand::Lock(uv_cli::LockArgs {
script: Some(script),
..
}) = &**command
{
Pep723Script::read(&script).await?.map(Pep723Item::Script)
} else if let ProjectCommand::Tree(uv_cli::TreeArgs {
script: Some(script),
..
}) = &**command
{
Pep723Script::read(&script).await?.map(Pep723Item::Script)
} else if let ProjectCommand::Export(uv_cli::ExportArgs {
script: Some(script),
..
}) = &**command
{
Pep723Script::read(&script).await?.map(Pep723Item::Script)
} else {
None
},
ProjectCommand::Remove(uv_cli::RemoveArgs {
script: Some(script),
..
})
| ProjectCommand::Lock(uv_cli::LockArgs {
script: Some(script),
..
})
| ProjectCommand::Tree(uv_cli::TreeArgs {
script: Some(script),
..
})
| ProjectCommand::Export(uv_cli::ExportArgs {
script: Some(script),
..
}) => match Pep723Script::read(&script).await {
Ok(Some(script)) => Some(Pep723Item::Script(script)),
Ok(None) => {
// TODO(charlie): `uv lock --script` should initialize the tag, if it doesn't
// exist.
bail!(
"`{}` does not contain a PEP 723 metadata tag; run `{}` to initialize the script",
script.user_display().cyan(),
format!("uv init --script {}", script.user_display()).green()
)
}
Err(Pep723Error::Io(err)) if err.kind() == std::io::ErrorKind::NotFound => {
bail!(
"Failed to read `{}` (not found); run `{}` to create a PEP 723 script",
script.user_display().cyan(),
format!("uv init --script {}", script.user_display()).green()
)
}
Err(err) => return Err(err.into()),
},
_ => None,
}
} else {
None

View File

@ -23092,6 +23092,37 @@ fn lock_script_path() -> Result<()> {
Ok(())
}
#[test]
fn lock_script_error() -> Result<()> {
let context = TestContext::new("3.12");
uv_snapshot!(context.filters(), context.lock().arg("--script").arg("script.py"), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Failed to read `script.py` (not found); run `uv init --script script.py` to create a PEP 723 script
"###);
let script = context.temp_dir.child("script.py");
script.write_str(indoc! { r"
import anyio
"
})?;
uv_snapshot!(context.filters(), context.lock().arg("--script").arg("script.py"), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: `script.py` does not contain a PEP 723 metadata tag; run `uv init --script script.py` to initialize the script
"###);
Ok(())
}
#[test]
fn lock_pytorch_cpu() -> Result<()> {
let context = TestContext::new("3.12").with_exclude_newer("2025-01-30T00:00:00Z");