diff --git a/crates/uv-scripts/src/lib.rs b/crates/uv-scripts/src/lib.rs index ad25ac131..860986b6c 100644 --- a/crates/uv-scripts/src/lib.rs +++ b/crates/uv-scripts/src/lib.rs @@ -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: pub async fn read(file: impl AsRef) -> Result, 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: pub async fn read(file: impl AsRef) -> Result, 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)] diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index 5fb16f72e..b24ac5de0 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -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 { // 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 diff --git a/crates/uv/tests/it/lock.rs b/crates/uv/tests/it/lock.rs index 6907d39cb..e79139299 100644 --- a/crates/uv/tests/it/lock.rs +++ b/crates/uv/tests/it/lock.rs @@ -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");