mirror of https://github.com/astral-sh/uv
Add a newline after metadata when initializing scripts with other metadata blocks (#12501)
## Summary uv doesn't separate the metadata block from other blocks when adding the `script` block to a script, which results in the next block being considered part of the script block and causes errors when running. See #12499 for more details. Closes #12499 ## Test Plan I manually tested the most common scenario, but there's a few edge cases that would be good to have tests for. I would have written the tests also, but I was running into errors like this: ```bash $ cargo test --package uv-scripts Compiling uv-configuration v0.0.1 (/home/merlin/Projects/uv/crates/uv-configuration) error: cannot find attribute `value` in this scope --> crates/uv-configuration/src/project_build_backend.rs:8:38 | 8 | #[cfg_attr(feature = "schemars", value(hide = true))] | ^^^^^ error: could not compile `uv-configuration` (lib) due to 1 previous error ``` --------- Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
This commit is contained in:
parent
9e10f83ce7
commit
5b2a8abef9
|
|
@ -158,7 +158,22 @@ impl Pep723Script {
|
|||
requires_python: &VersionSpecifiers,
|
||||
) -> Result<Self, Pep723Error> {
|
||||
let contents = fs_err::tokio::read(&file).await?;
|
||||
let (prelude, metadata, postlude) = Self::init_metadata(&contents, requires_python)?;
|
||||
Ok(Self {
|
||||
path: std::path::absolute(file)?,
|
||||
metadata,
|
||||
prelude,
|
||||
postlude,
|
||||
})
|
||||
}
|
||||
|
||||
/// Generates a default PEP 723 metadata table from the provided script contents.
|
||||
///
|
||||
/// See: <https://peps.python.org/pep-0723/>
|
||||
pub fn init_metadata(
|
||||
contents: &[u8],
|
||||
requires_python: &VersionSpecifiers,
|
||||
) -> Result<(String, Pep723Metadata, String), Pep723Error> {
|
||||
// Define the default metadata.
|
||||
let default_metadata = indoc::formatdoc! {r#"
|
||||
requires-python = "{requires_python}"
|
||||
|
|
@ -168,19 +183,30 @@ impl Pep723Script {
|
|||
};
|
||||
let metadata = Pep723Metadata::from_str(&default_metadata)?;
|
||||
|
||||
// Extract the shebang and script content.
|
||||
let (shebang, postlude) = extract_shebang(&contents)?;
|
||||
// Extract the shebang and script content.
|
||||
let (shebang, postlude) = extract_shebang(contents)?;
|
||||
|
||||
Ok(Self {
|
||||
path: std::path::absolute(file)?,
|
||||
prelude: if shebang.is_empty() {
|
||||
// Add a newline to the beginning if it starts with a valid metadata comment line.
|
||||
let postlude = if postlude.strip_prefix('#').is_some_and(|postlude| {
|
||||
postlude
|
||||
.chars()
|
||||
.next()
|
||||
.is_some_and(|c| matches!(c, ' ' | '\r' | '\n'))
|
||||
}) {
|
||||
format!("\n{postlude}")
|
||||
} else {
|
||||
postlude
|
||||
};
|
||||
|
||||
Ok((
|
||||
if shebang.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!("{shebang}\n")
|
||||
},
|
||||
metadata,
|
||||
postlude,
|
||||
})
|
||||
))
|
||||
}
|
||||
|
||||
/// Create a PEP 723 script at the given path.
|
||||
|
|
@ -554,7 +580,7 @@ fn serialize_metadata(metadata: &str) -> String {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{serialize_metadata, Pep723Error, ScriptTag};
|
||||
use crate::{serialize_metadata, Pep723Error, Pep723Script, ScriptTag};
|
||||
|
||||
#[test]
|
||||
fn missing_space() {
|
||||
|
|
@ -688,6 +714,7 @@ mod tests {
|
|||
assert_eq!(actual.metadata, expected_metadata);
|
||||
assert_eq!(actual.postlude, expected_data);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn embedded_comment() {
|
||||
let contents = indoc::indoc! {r"
|
||||
|
|
@ -751,7 +778,7 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialize_metadata_formatting() {
|
||||
fn serialize_metadata_formatting() {
|
||||
let metadata = indoc::indoc! {r"
|
||||
requires-python = '>=3.11'
|
||||
dependencies = [
|
||||
|
|
@ -775,11 +802,250 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialize_metadata_empty() {
|
||||
fn serialize_metadata_empty() {
|
||||
let metadata = "";
|
||||
let expected_output = "# /// script\n# ///\n";
|
||||
|
||||
let result = serialize_metadata(metadata);
|
||||
assert_eq!(result, expected_output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn script_init_empty() {
|
||||
let contents = "".as_bytes();
|
||||
let (prelude, metadata, postlude) =
|
||||
Pep723Script::init_metadata(contents, &uv_pep440::VersionSpecifiers::default())
|
||||
.unwrap();
|
||||
assert_eq!(prelude, "");
|
||||
assert_eq!(
|
||||
metadata.raw,
|
||||
indoc::indoc! {r#"
|
||||
requires-python = ""
|
||||
dependencies = []
|
||||
"#}
|
||||
);
|
||||
assert_eq!(postlude, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn script_init_with_hashbang() {
|
||||
let contents = indoc::indoc! {r#"
|
||||
#!/usr/bin/env python3
|
||||
|
||||
print("Hello, world!")
|
||||
"#}
|
||||
.as_bytes();
|
||||
let (prelude, metadata, postlude) =
|
||||
Pep723Script::init_metadata(contents, &uv_pep440::VersionSpecifiers::default())
|
||||
.unwrap();
|
||||
assert_eq!(prelude, "#!/usr/bin/env python3\n");
|
||||
assert_eq!(
|
||||
metadata.raw,
|
||||
indoc::indoc! {r#"
|
||||
requires-python = ""
|
||||
dependencies = []
|
||||
"#}
|
||||
);
|
||||
assert_eq!(
|
||||
postlude,
|
||||
indoc::indoc! {r#"
|
||||
|
||||
print("Hello, world!")
|
||||
"#}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn script_init_with_other_metadata() {
|
||||
let contents = indoc::indoc! {r#"
|
||||
# /// noscript
|
||||
# Hello,
|
||||
#
|
||||
# World!
|
||||
# ///
|
||||
|
||||
print("Hello, world!")
|
||||
"#}
|
||||
.as_bytes();
|
||||
let (prelude, metadata, postlude) =
|
||||
Pep723Script::init_metadata(contents, &uv_pep440::VersionSpecifiers::default())
|
||||
.unwrap();
|
||||
assert_eq!(prelude, "");
|
||||
assert_eq!(
|
||||
metadata.raw,
|
||||
indoc::indoc! {r#"
|
||||
requires-python = ""
|
||||
dependencies = []
|
||||
"#}
|
||||
);
|
||||
// Note the extra line at the beginning
|
||||
assert_eq!(
|
||||
postlude,
|
||||
indoc::indoc! {r#"
|
||||
|
||||
# /// noscript
|
||||
# Hello,
|
||||
#
|
||||
# World!
|
||||
# ///
|
||||
|
||||
print("Hello, world!")
|
||||
"#}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn script_init_with_hashbang_and_other_metadata() {
|
||||
let contents = indoc::indoc! {r#"
|
||||
#!/usr/bin/env python3
|
||||
# /// noscript
|
||||
# Hello,
|
||||
#
|
||||
# World!
|
||||
# ///
|
||||
|
||||
print("Hello, world!")
|
||||
"#}
|
||||
.as_bytes();
|
||||
let (prelude, metadata, postlude) =
|
||||
Pep723Script::init_metadata(contents, &uv_pep440::VersionSpecifiers::default())
|
||||
.unwrap();
|
||||
assert_eq!(prelude, "#!/usr/bin/env python3\n");
|
||||
assert_eq!(
|
||||
metadata.raw,
|
||||
indoc::indoc! {r#"
|
||||
requires-python = ""
|
||||
dependencies = []
|
||||
"#}
|
||||
);
|
||||
// Note the extra line at the beginning.
|
||||
assert_eq!(
|
||||
postlude,
|
||||
indoc::indoc! {r#"
|
||||
|
||||
# /// noscript
|
||||
# Hello,
|
||||
#
|
||||
# World!
|
||||
# ///
|
||||
|
||||
print("Hello, world!")
|
||||
"#}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn script_init_with_valid_metadata_line() {
|
||||
let contents = indoc::indoc! {r#"
|
||||
# Hello,
|
||||
# /// noscript
|
||||
#
|
||||
# World!
|
||||
# ///
|
||||
|
||||
print("Hello, world!")
|
||||
"#}
|
||||
.as_bytes();
|
||||
let (prelude, metadata, postlude) =
|
||||
Pep723Script::init_metadata(contents, &uv_pep440::VersionSpecifiers::default())
|
||||
.unwrap();
|
||||
assert_eq!(prelude, "");
|
||||
assert_eq!(
|
||||
metadata.raw,
|
||||
indoc::indoc! {r#"
|
||||
requires-python = ""
|
||||
dependencies = []
|
||||
"#}
|
||||
);
|
||||
// Note the extra line at the beginning
|
||||
assert_eq!(
|
||||
postlude,
|
||||
indoc::indoc! {r#"
|
||||
|
||||
# Hello,
|
||||
# /// noscript
|
||||
#
|
||||
# World!
|
||||
# ///
|
||||
|
||||
print("Hello, world!")
|
||||
"#}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn script_init_with_valid_empty_metadata_line() {
|
||||
let contents = indoc::indoc! {r#"
|
||||
#
|
||||
# /// noscript
|
||||
# Hello,
|
||||
# World!
|
||||
# ///
|
||||
|
||||
print("Hello, world!")
|
||||
"#}
|
||||
.as_bytes();
|
||||
let (prelude, metadata, postlude) =
|
||||
Pep723Script::init_metadata(contents, &uv_pep440::VersionSpecifiers::default())
|
||||
.unwrap();
|
||||
assert_eq!(prelude, "");
|
||||
assert_eq!(
|
||||
metadata.raw,
|
||||
indoc::indoc! {r#"
|
||||
requires-python = ""
|
||||
dependencies = []
|
||||
"#}
|
||||
);
|
||||
// Note the extra line at the beginning
|
||||
assert_eq!(
|
||||
postlude,
|
||||
indoc::indoc! {r#"
|
||||
|
||||
#
|
||||
# /// noscript
|
||||
# Hello,
|
||||
# World!
|
||||
# ///
|
||||
|
||||
print("Hello, world!")
|
||||
"#}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn script_init_with_non_metadata_comment() {
|
||||
let contents = indoc::indoc! {r#"
|
||||
#Hello,
|
||||
# /// noscript
|
||||
#
|
||||
# World!
|
||||
# ///
|
||||
|
||||
print("Hello, world!")
|
||||
"#}
|
||||
.as_bytes();
|
||||
let (prelude, metadata, postlude) =
|
||||
Pep723Script::init_metadata(contents, &uv_pep440::VersionSpecifiers::default())
|
||||
.unwrap();
|
||||
assert_eq!(prelude, "");
|
||||
assert_eq!(
|
||||
metadata.raw,
|
||||
indoc::indoc! {r#"
|
||||
requires-python = ""
|
||||
dependencies = []
|
||||
"#}
|
||||
);
|
||||
assert_eq!(
|
||||
postlude,
|
||||
indoc::indoc! {r#"
|
||||
#Hello,
|
||||
# /// noscript
|
||||
#
|
||||
# World!
|
||||
# ///
|
||||
|
||||
print("Hello, world!")
|
||||
"#}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue