Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support PEP 723 metadata with uv run - #8111

Merged
merged 3 commits into from
Oct 10, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 28 additions & 10 deletions crates/uv-scripts/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,17 @@ use uv_workspace::pyproject::Sources;

static FINDER: LazyLock<Finder> = LazyLock::new(|| Finder::new(b"# /// script"));

#[derive(Debug)]
pub enum Source {
File(PathBuf),
Stdin,
}

/// A PEP 723 script, including its [`Pep723Metadata`].
#[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 +40,26 @@ 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(Source::File(file.as_ref().to_path_buf()), &contents)
}
Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(None),
Err(err) => Err(err.into()),
}
}

pub fn parse_stdin(contents: &[u8]) -> Result<Option<Self>, Pep723Error> {
Self::parse_contents(Source::Stdin, contents)
}

fn parse_contents(source: Source, contents: &[u8]) -> 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 +69,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 +98,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,7 +163,11 @@ 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(())
}
}

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 {
bail!("Cannot resolve requirement from stdin")
};
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!(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tried to move the match within writelin! but couldn't get the arm types to match.

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()?,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the current dir makes sense here.

};

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(())
}
Loading