Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
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!(
Copy link
Copy Markdown
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
Copy Markdown
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(())
}