diff --git a/crates/uv-scripts/src/lib.rs b/crates/uv-scripts/src/lib.rs index 470c452f83b2..5431a1e64994 100644 --- a/crates/uv-scripts/src/lib.rs +++ b/crates/uv-scripts/src/lib.rs @@ -5,10 +5,10 @@ use std::str::FromStr; use std::sync::LazyLock; use memchr::memmem::Finder; -use pep440_rs::VersionSpecifiers; use serde::Deserialize; use thiserror::Error; +use pep440_rs::VersionSpecifiers; use pep508_rs::PackageName; use pypi_types::VerbatimParsedUrl; use uv_settings::{GlobalOptions, ResolverInstallerOptions}; @@ -41,13 +41,14 @@ impl Pep723Script { }; // Extract the `script` tag. - let Some(ScriptTag { + let ScriptTag { prelude, metadata, postlude, - }) = ScriptTag::parse(&contents)? - else { - return Ok(None); + } = match ScriptTag::parse(&contents) { + Ok(Some(tag)) => tag, + Ok(None) => return Ok(None), + Err(err) => return Err(err), }; // Parse the metadata. @@ -152,6 +153,8 @@ pub struct ToolUv { #[derive(Debug, Error)] 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(transparent)] Io(#[from] io::Error), #[error(transparent)] @@ -272,7 +275,7 @@ impl ScriptTag { // // The latter `///` is the closing pragma let Some(index) = toml.iter().rev().position(|line| *line == "///") else { - return Ok(None); + return Err(Pep723Error::UnclosedBlock); }; let index = toml.len() - index; @@ -364,7 +367,7 @@ fn serialize_metadata(metadata: &str) -> String { #[cfg(test)] mod tests { - use crate::{serialize_metadata, ScriptTag}; + use crate::{serialize_metadata, Pep723Error, ScriptTag}; #[test] fn missing_space() { @@ -374,7 +377,10 @@ mod tests { # /// "}; - assert_eq!(ScriptTag::parse(contents.as_bytes()).unwrap(), None); + assert!(matches!( + ScriptTag::parse(contents.as_bytes()), + Err(Pep723Error::UnclosedBlock) + )); } #[test] @@ -388,7 +394,10 @@ mod tests { # ] "}; - assert_eq!(ScriptTag::parse(contents.as_bytes()).unwrap(), None); + assert!(matches!( + ScriptTag::parse(contents.as_bytes()), + Err(Pep723Error::UnclosedBlock) + )); } #[test] diff --git a/crates/uv/tests/run.rs b/crates/uv/tests/run.rs index d6e04fa99f01..f482aa8589ad 100644 --- a/crates/uv/tests/run.rs +++ b/crates/uv/tests/run.rs @@ -347,6 +347,30 @@ fn run_pep723_script() -> Result<()> { ╰─▶ Because there are no versions of add and you require add, we can conclude that your requirements are unsatisfiable. "###); + // If the script contains an unclosed PEP 723 tag, we should error. + let test_script = context.temp_dir.child("main.py"); + test_script.write_str(indoc! { r#" + # /// script + # requires-python = ">=3.11" + # dependencies = [ + # "iniconfig", + # ] + + # /// + + import iniconfig + "# + })?; + + uv_snapshot!(context.filters(), context.run().arg("--no-project").arg("main.py"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + 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 `#`. + "###); + Ok(()) }