Skip to content

Commit

Permalink
Support PEP 723 metadata with uv run - (#8111)
Browse files Browse the repository at this point in the history
## Summary

Fixes #8097. One challenge is that the `Pep723Script` is used for both
reading
and writing the metadata, so I wasn't sure about how to handle
`script.write`
when stdin (currently just ignoring it, but maybe we should raise an
error?).

## Test Plan

Added a test case copying the `test_stdin` with PEP 723 metadata.
  • Loading branch information
manzt authored Oct 10, 2024
1 parent 0627b4a commit e377563
Show file tree
Hide file tree
Showing 6 changed files with 109 additions and 36 deletions.
43 changes: 33 additions & 10 deletions crates/uv-scripts/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ static FINDER: LazyLock<Finder> = LazyLock::new(|| Finder::new(b"# /// script"))
#[derive(Debug)]
pub struct Pep723Script {
/// The path to the Python script.
pub path: PathBuf,
pub source: Source,
/// The parsed [`Pep723Metadata`] table from the script.
pub metadata: Pep723Metadata,
/// The content of the script before the metadata table.
Expand All @@ -34,18 +34,28 @@ impl Pep723Script {
///
/// 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()),
};
match fs_err::tokio::read(&file).await {
Ok(contents) => {
Self::parse_contents(&contents, Source::File(file.as_ref().to_path_buf()))
}
Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(None),
Err(err) => Err(err.into()),
}
}

/// Read the PEP 723 `script` metadata from stdin.
pub fn parse_stdin(contents: &[u8]) -> Result<Option<Self>, Pep723Error> {
Self::parse_contents(contents, Source::Stdin)
}

/// Parse the contents of a Python script and extract the `script` metadata block.
fn parse_contents(contents: &[u8], source: Source) -> Result<Option<Self>, Pep723Error> {
// Extract the `script` tag.
let ScriptTag {
prelude,
metadata,
postlude,
} = match ScriptTag::parse(&contents) {
} = match ScriptTag::parse(contents) {
Ok(Some(tag)) => tag,
Ok(None) => return Ok(None),
Err(err) => return Err(err),
Expand All @@ -55,7 +65,7 @@ impl Pep723Script {
let metadata = Pep723Metadata::from_str(&metadata)?;

Ok(Some(Self {
path: file.as_ref().to_path_buf(),
source,
metadata,
prelude,
postlude,
Expand Down Expand Up @@ -84,7 +94,7 @@ impl Pep723Script {
let (shebang, postlude) = extract_shebang(&contents)?;

Ok(Self {
path: file.as_ref().to_path_buf(),
source: Source::File(file.as_ref().to_path_buf()),
prelude: if shebang.is_empty() {
String::new()
} else {
Expand Down Expand Up @@ -149,10 +159,23 @@ impl Pep723Script {
self.postlude
);

Ok(fs_err::tokio::write(&self.path, content).await?)
if let Source::File(path) = &self.source {
fs_err::tokio::write(&path, content).await?;
}

Ok(())
}
}

/// The source of a PEP 723 script.
#[derive(Debug)]
pub enum Source {
/// The PEP 723 script is sourced from a file.
File(PathBuf),
/// The PEP 723 script is sourced from stdin.
Stdin,
}

/// PEP 723 metadata as parsed from a `script` comment block.
///
/// See: <https://peps.python.org/pep-0723/>
Expand Down
13 changes: 7 additions & 6 deletions crates/uv/src/commands/project/add.rs
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,10 @@ pub(crate) async fn add(
(uv_pep508::Requirement::from(requirement), None)
}
Target::Script(ref script, _) => {
let script_path = std::path::absolute(&script.path)?;
let uv_scripts::Source::File(path) = &script.source else {
unreachable!("script source is not a file");
};
let script_path = std::path::absolute(path)?;
let script_dir = script_path.parent().expect("script path has no parent");
resolve_requirement(
requirement,
Expand Down Expand Up @@ -508,11 +511,9 @@ pub(crate) async fn add(
Target::Project(project, venv) => (project, venv),
// If `--script`, exit early. There's no reason to lock and sync.
Target::Script(script, _) => {
writeln!(
printer.stderr(),
"Updated `{}`",
script.path.user_display().cyan()
)?;
if let uv_scripts::Source::File(path) = &script.source {
writeln!(printer.stderr(), "Updated `{}`", path.user_display().cyan())?;
}
return Ok(ExitStatus::Success);
}
};
Expand Down
8 changes: 3 additions & 5 deletions crates/uv/src/commands/project/remove.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,11 +144,9 @@ pub(crate) async fn remove(
Target::Project(project) => project,
// If `--script`, exit early. There's no reason to lock and sync.
Target::Script(script) => {
writeln!(
printer.stderr(),
"Updated `{}`",
script.path.user_display().cyan()
)?;
if let uv_scripts::Source::File(path) = &script.source {
writeln!(printer.stderr(), "Updated `{}`", path.user_display().cyan())?;
}
return Ok(ExitStatus::Success);
}
};
Expand Down
32 changes: 24 additions & 8 deletions crates/uv/src/commands/project/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,11 +107,19 @@ pub(crate) async fn run(
// Determine whether the command to execute is a PEP 723 script.
let temp_dir;
let script_interpreter = if let Some(script) = script {
writeln!(
printer.stderr(),
"Reading inline script metadata from: {}",
script.path.user_display().cyan()
)?;
if let uv_scripts::Source::File(path) = &script.source {
writeln!(
printer.stderr(),
"Reading inline script metadata from: {}",
path.user_display().cyan()
)?;
} else {
writeln!(
printer.stderr(),
"Reading inline script metadata from: `{}`",
"stdin".cyan()
)?;
}

let (source, python_request) = if let Some(request) = python.as_deref() {
// (1) Explicit request from user
Expand Down Expand Up @@ -196,15 +204,23 @@ pub(crate) async fn run(
.unwrap_or(&empty),
SourceStrategy::Disabled => &empty,
};
let script_path = std::path::absolute(script.path)?;
let script_dir = script_path.parent().expect("script path has no parent");
let script_dir = match &script.source {
uv_scripts::Source::File(path) => {
let script_path = std::path::absolute(path)?;
script_path
.parent()
.expect("script path has no parent")
.to_owned()
}
uv_scripts::Source::Stdin => std::env::current_dir()?,
};

let requirements = dependencies
.into_iter()
.flat_map(|requirement| {
LoweredRequirement::from_non_workspace_requirement(
requirement,
script_dir,
script_dir.as_ref(),
script_sources,
)
.map_ok(LoweredRequirement::into_inner)
Expand Down
13 changes: 6 additions & 7 deletions crates/uv/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -214,13 +214,12 @@ 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 {
if let Some(
RunCommand::PythonScript(script, _) | RunCommand::PythonGuiScript(script, _),
) = run_command.as_ref()
{
Pep723Script::read(&script).await?
} else {
None
match run_command.as_ref() {
Some(
RunCommand::PythonScript(script, _) | RunCommand::PythonGuiScript(script, _),
) => Pep723Script::read(&script).await?,
Some(RunCommand::PythonStdin(contents)) => Pep723Script::parse_stdin(contents)?,
_ => None,
}
} else if let ProjectCommand::Remove(uv_cli::RemoveArgs {
script: Some(script),
Expand Down
36 changes: 36 additions & 0 deletions crates/uv/tests/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2359,3 +2359,39 @@ fn run_url_like_with_local_file_priority() -> Result<()> {

Ok(())
}

#[test]
fn run_stdin_with_pep723() -> Result<()> {
let context = TestContext::new("3.12");

let test_script = context.temp_dir.child("main.py");
test_script.write_str(indoc! { r#"
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "iniconfig",
# ]
# ///
import iniconfig
print("Hello, world!")
"#
})?;

let mut command = context.run();
let command_with_args = command.stdin(std::fs::File::open(test_script)?).arg("-");
uv_snapshot!(context.filters(), command_with_args, @r###"
success: true
exit_code: 0
----- stdout -----
Hello, world!
----- stderr -----
Reading inline script metadata from: `stdin`
Resolved 1 package in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ iniconfig==2.0.0
"###);

Ok(())
}

0 comments on commit e377563

Please sign in to comment.