mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-03 02:22:19 +00:00
Support PEP 723 metadata with uv run -
(#8111)
## 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.
This commit is contained in:
parent
0627b4a8a4
commit
e3775635d4
6 changed files with 109 additions and 36 deletions
|
@ -20,7 +20,7 @@ static FINDER: LazyLock<Finder> = LazyLock::new(|| Finder::new(b"# /// script"))
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Pep723Script {
|
pub struct Pep723Script {
|
||||||
/// The path to the Python script.
|
/// The path to the Python script.
|
||||||
pub path: PathBuf,
|
pub source: Source,
|
||||||
/// The parsed [`Pep723Metadata`] table from the script.
|
/// The parsed [`Pep723Metadata`] table from the script.
|
||||||
pub metadata: Pep723Metadata,
|
pub metadata: Pep723Metadata,
|
||||||
/// The content of the script before the metadata table.
|
/// The content of the script before the metadata table.
|
||||||
|
@ -34,18 +34,28 @@ impl Pep723Script {
|
||||||
///
|
///
|
||||||
/// See: <https://peps.python.org/pep-0723/>
|
/// See: <https://peps.python.org/pep-0723/>
|
||||||
pub async fn read(file: impl AsRef<Path>) -> Result<Option<Self>, Pep723Error> {
|
pub async fn read(file: impl AsRef<Path>) -> Result<Option<Self>, Pep723Error> {
|
||||||
let contents = match fs_err::tokio::read(&file).await {
|
match fs_err::tokio::read(&file).await {
|
||||||
Ok(contents) => contents,
|
Ok(contents) => {
|
||||||
Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(None),
|
Self::parse_contents(&contents, Source::File(file.as_ref().to_path_buf()))
|
||||||
Err(err) => return Err(err.into()),
|
}
|
||||||
};
|
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.
|
// Extract the `script` tag.
|
||||||
let ScriptTag {
|
let ScriptTag {
|
||||||
prelude,
|
prelude,
|
||||||
metadata,
|
metadata,
|
||||||
postlude,
|
postlude,
|
||||||
} = match ScriptTag::parse(&contents) {
|
} = match ScriptTag::parse(contents) {
|
||||||
Ok(Some(tag)) => tag,
|
Ok(Some(tag)) => tag,
|
||||||
Ok(None) => return Ok(None),
|
Ok(None) => return Ok(None),
|
||||||
Err(err) => return Err(err),
|
Err(err) => return Err(err),
|
||||||
|
@ -55,7 +65,7 @@ impl Pep723Script {
|
||||||
let metadata = Pep723Metadata::from_str(&metadata)?;
|
let metadata = Pep723Metadata::from_str(&metadata)?;
|
||||||
|
|
||||||
Ok(Some(Self {
|
Ok(Some(Self {
|
||||||
path: file.as_ref().to_path_buf(),
|
source,
|
||||||
metadata,
|
metadata,
|
||||||
prelude,
|
prelude,
|
||||||
postlude,
|
postlude,
|
||||||
|
@ -84,7 +94,7 @@ impl Pep723Script {
|
||||||
let (shebang, postlude) = extract_shebang(&contents)?;
|
let (shebang, postlude) = extract_shebang(&contents)?;
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
path: file.as_ref().to_path_buf(),
|
source: Source::File(file.as_ref().to_path_buf()),
|
||||||
prelude: if shebang.is_empty() {
|
prelude: if shebang.is_empty() {
|
||||||
String::new()
|
String::new()
|
||||||
} else {
|
} else {
|
||||||
|
@ -149,10 +159,23 @@ impl Pep723Script {
|
||||||
self.postlude
|
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.
|
/// PEP 723 metadata as parsed from a `script` comment block.
|
||||||
///
|
///
|
||||||
/// See: <https://peps.python.org/pep-0723/>
|
/// See: <https://peps.python.org/pep-0723/>
|
||||||
|
|
|
@ -379,7 +379,10 @@ pub(crate) async fn add(
|
||||||
(uv_pep508::Requirement::from(requirement), None)
|
(uv_pep508::Requirement::from(requirement), None)
|
||||||
}
|
}
|
||||||
Target::Script(ref script, _) => {
|
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");
|
let script_dir = script_path.parent().expect("script path has no parent");
|
||||||
resolve_requirement(
|
resolve_requirement(
|
||||||
requirement,
|
requirement,
|
||||||
|
@ -508,11 +511,9 @@ pub(crate) async fn add(
|
||||||
Target::Project(project, venv) => (project, venv),
|
Target::Project(project, venv) => (project, venv),
|
||||||
// If `--script`, exit early. There's no reason to lock and sync.
|
// If `--script`, exit early. There's no reason to lock and sync.
|
||||||
Target::Script(script, _) => {
|
Target::Script(script, _) => {
|
||||||
writeln!(
|
if let uv_scripts::Source::File(path) = &script.source {
|
||||||
printer.stderr(),
|
writeln!(printer.stderr(), "Updated `{}`", path.user_display().cyan())?;
|
||||||
"Updated `{}`",
|
}
|
||||||
script.path.user_display().cyan()
|
|
||||||
)?;
|
|
||||||
return Ok(ExitStatus::Success);
|
return Ok(ExitStatus::Success);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -144,11 +144,9 @@ pub(crate) async fn remove(
|
||||||
Target::Project(project) => project,
|
Target::Project(project) => project,
|
||||||
// If `--script`, exit early. There's no reason to lock and sync.
|
// If `--script`, exit early. There's no reason to lock and sync.
|
||||||
Target::Script(script) => {
|
Target::Script(script) => {
|
||||||
writeln!(
|
if let uv_scripts::Source::File(path) = &script.source {
|
||||||
printer.stderr(),
|
writeln!(printer.stderr(), "Updated `{}`", path.user_display().cyan())?;
|
||||||
"Updated `{}`",
|
}
|
||||||
script.path.user_display().cyan()
|
|
||||||
)?;
|
|
||||||
return Ok(ExitStatus::Success);
|
return Ok(ExitStatus::Success);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -107,11 +107,19 @@ pub(crate) async fn run(
|
||||||
// Determine whether the command to execute is a PEP 723 script.
|
// Determine whether the command to execute is a PEP 723 script.
|
||||||
let temp_dir;
|
let temp_dir;
|
||||||
let script_interpreter = if let Some(script) = script {
|
let script_interpreter = if let Some(script) = script {
|
||||||
writeln!(
|
if let uv_scripts::Source::File(path) = &script.source {
|
||||||
printer.stderr(),
|
writeln!(
|
||||||
"Reading inline script metadata from: {}",
|
printer.stderr(),
|
||||||
script.path.user_display().cyan()
|
"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() {
|
let (source, python_request) = if let Some(request) = python.as_deref() {
|
||||||
// (1) Explicit request from user
|
// (1) Explicit request from user
|
||||||
|
@ -196,15 +204,23 @@ pub(crate) async fn run(
|
||||||
.unwrap_or(&empty),
|
.unwrap_or(&empty),
|
||||||
SourceStrategy::Disabled => &empty,
|
SourceStrategy::Disabled => &empty,
|
||||||
};
|
};
|
||||||
let script_path = std::path::absolute(script.path)?;
|
let script_dir = match &script.source {
|
||||||
let script_dir = script_path.parent().expect("script path has no parent");
|
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
|
let requirements = dependencies
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.flat_map(|requirement| {
|
.flat_map(|requirement| {
|
||||||
LoweredRequirement::from_non_workspace_requirement(
|
LoweredRequirement::from_non_workspace_requirement(
|
||||||
requirement,
|
requirement,
|
||||||
script_dir,
|
script_dir.as_ref(),
|
||||||
script_sources,
|
script_sources,
|
||||||
)
|
)
|
||||||
.map_ok(LoweredRequirement::into_inner)
|
.map_ok(LoweredRequirement::into_inner)
|
||||||
|
|
|
@ -214,13 +214,12 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
|
||||||
// If the target is a PEP 723 script, parse it.
|
// If the target is a PEP 723 script, parse it.
|
||||||
let script = if let Commands::Project(command) = &*cli.command {
|
let script = if let Commands::Project(command) = &*cli.command {
|
||||||
if let ProjectCommand::Run(uv_cli::RunArgs { .. }) = &**command {
|
if let ProjectCommand::Run(uv_cli::RunArgs { .. }) = &**command {
|
||||||
if let Some(
|
match run_command.as_ref() {
|
||||||
RunCommand::PythonScript(script, _) | RunCommand::PythonGuiScript(script, _),
|
Some(
|
||||||
) = run_command.as_ref()
|
RunCommand::PythonScript(script, _) | RunCommand::PythonGuiScript(script, _),
|
||||||
{
|
) => Pep723Script::read(&script).await?,
|
||||||
Pep723Script::read(&script).await?
|
Some(RunCommand::PythonStdin(contents)) => Pep723Script::parse_stdin(contents)?,
|
||||||
} else {
|
_ => None,
|
||||||
None
|
|
||||||
}
|
}
|
||||||
} else if let ProjectCommand::Remove(uv_cli::RemoveArgs {
|
} else if let ProjectCommand::Remove(uv_cli::RemoveArgs {
|
||||||
script: Some(script),
|
script: Some(script),
|
||||||
|
|
|
@ -2359,3 +2359,39 @@ fn run_url_like_with_local_file_priority() -> Result<()> {
|
||||||
|
|
||||||
Ok(())
|
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…
Add table
Add a link
Reference in a new issue