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");