1use std::collections::BTreeMap;
2use std::io;
3use std::path::{Path, PathBuf};
4use std::str::FromStr;
5use std::sync::LazyLock;
6
7use memchr::memmem::Finder;
8use serde::Deserialize;
9use thiserror::Error;
10use url::Url;
11
12use uv_configuration::NoSources;
13use uv_normalize::PackageName;
14use uv_pep440::VersionSpecifiers;
15use uv_pypi_types::VerbatimParsedUrl;
16use uv_redacted::DisplaySafeUrl;
17use uv_settings::{GlobalOptions, ResolverInstallerSchema};
18use uv_warnings::warn_user;
19use uv_workspace::pyproject::{ExtraBuildDependency, Sources};
20
21static FINDER: LazyLock<Finder> = LazyLock::new(|| Finder::new(b"# /// script"));
22
23#[derive(Debug)]
25pub enum Pep723Item {
26 Script(Pep723Script),
28 Stdin(Pep723Metadata),
30 Remote(Pep723Metadata, DisplaySafeUrl),
32}
33
34impl Pep723Item {
35 pub fn metadata(&self) -> &Pep723Metadata {
37 match self {
38 Self::Script(script) => &script.metadata,
39 Self::Stdin(metadata) => metadata,
40 Self::Remote(metadata, ..) => metadata,
41 }
42 }
43
44 pub fn into_metadata(self) -> Pep723Metadata {
46 match self {
47 Self::Script(script) => script.metadata,
48 Self::Stdin(metadata) => metadata,
49 Self::Remote(metadata, ..) => metadata,
50 }
51 }
52
53 pub fn path(&self) -> Option<&Path> {
55 match self {
56 Self::Script(script) => Some(&script.path),
57 Self::Stdin(..) => None,
58 Self::Remote(..) => None,
59 }
60 }
61
62 pub fn as_script(&self) -> Option<&Pep723Script> {
64 match self {
65 Self::Script(script) => Some(script),
66 _ => None,
67 }
68 }
69}
70
71#[derive(Debug, Copy, Clone)]
73pub enum Pep723ItemRef<'item> {
74 Script(&'item Pep723Script),
76 Stdin(&'item Pep723Metadata),
78 Remote(&'item Pep723Metadata, &'item Url),
80}
81
82impl Pep723ItemRef<'_> {
83 pub fn metadata(&self) -> &Pep723Metadata {
85 match self {
86 Self::Script(script) => &script.metadata,
87 Self::Stdin(metadata) => metadata,
88 Self::Remote(metadata, ..) => metadata,
89 }
90 }
91
92 pub fn path(&self) -> Option<&Path> {
94 match self {
95 Self::Script(script) => Some(&script.path),
96 Self::Stdin(..) => None,
97 Self::Remote(..) => None,
98 }
99 }
100
101 pub fn directory(&self) -> Result<PathBuf, io::Error> {
103 match self {
104 Self::Script(script) => Ok(std::path::absolute(&script.path)?
105 .parent()
106 .expect("script path has no parent")
107 .to_owned()),
108 Self::Stdin(..) | Self::Remote(..) => std::env::current_dir(),
109 }
110 }
111
112 pub fn indexes(&self, source_strategy: &NoSources) -> &[uv_distribution_types::Index] {
114 match source_strategy {
115 NoSources::None => self
116 .metadata()
117 .tool
118 .as_ref()
119 .and_then(|tool| tool.uv.as_ref())
120 .and_then(|uv| uv.top_level.index.as_deref())
121 .unwrap_or(&[]),
122 NoSources::All | NoSources::Packages(_) => &[],
123 }
124 }
125
126 pub fn sources(&self, source_strategy: &NoSources) -> &BTreeMap<PackageName, Sources> {
128 static EMPTY: BTreeMap<PackageName, Sources> = BTreeMap::new();
129 match source_strategy {
130 NoSources::None => self
131 .metadata()
132 .tool
133 .as_ref()
134 .and_then(|tool| tool.uv.as_ref())
135 .and_then(|uv| uv.sources.as_ref())
136 .unwrap_or(&EMPTY),
137 NoSources::All | NoSources::Packages(_) => &EMPTY,
138 }
139 }
140}
141
142impl<'item> From<&'item Pep723Item> for Pep723ItemRef<'item> {
143 fn from(item: &'item Pep723Item) -> Self {
144 match item {
145 Pep723Item::Script(script) => Self::Script(script),
146 Pep723Item::Stdin(metadata) => Self::Stdin(metadata),
147 Pep723Item::Remote(metadata, url) => Self::Remote(metadata, url),
148 }
149 }
150}
151
152impl<'item> From<&'item Pep723Script> for Pep723ItemRef<'item> {
153 fn from(script: &'item Pep723Script) -> Self {
154 Self::Script(script)
155 }
156}
157
158#[derive(Debug, Clone)]
160pub struct Pep723Script {
161 pub path: PathBuf,
163 pub metadata: Pep723Metadata,
165 pub prelude: String,
167 pub postlude: String,
169}
170
171impl Pep723Script {
172 pub async fn read(file: impl AsRef<Path>) -> Result<Option<Self>, Pep723Error> {
178 let contents = fs_err::tokio::read(&file).await?;
179
180 let ScriptTag {
182 prelude,
183 metadata,
184 postlude,
185 } = match ScriptTag::parse(&contents) {
186 Ok(Some(tag)) => tag,
187 Ok(None) => return Ok(None),
188 Err(err) => return Err(err),
189 };
190
191 let metadata = Pep723Metadata::from_str(&metadata)?;
193
194 Ok(Some(Self {
195 path: std::path::absolute(file)?,
196 metadata,
197 prelude,
198 postlude,
199 }))
200 }
201
202 pub async fn init(
206 file: impl AsRef<Path>,
207 requires_python: &VersionSpecifiers,
208 ) -> Result<Self, Pep723Error> {
209 let contents = fs_err::tokio::read(&file).await?;
210 let (prelude, metadata, postlude) = Self::init_metadata(&contents, requires_python)?;
211 Ok(Self {
212 path: std::path::absolute(file)?,
213 metadata,
214 prelude,
215 postlude,
216 })
217 }
218
219 pub fn init_metadata(
223 contents: &[u8],
224 requires_python: &VersionSpecifiers,
225 ) -> Result<(String, Pep723Metadata, String), Pep723Error> {
226 let default_metadata = if requires_python.is_empty() {
228 indoc::formatdoc! {r"
229 dependencies = []
230 ",
231 }
232 } else {
233 indoc::formatdoc! {r#"
234 requires-python = "{requires_python}"
235 dependencies = []
236 "#,
237 requires_python = requires_python,
238 }
239 };
240 let metadata = Pep723Metadata::from_str(&default_metadata)?;
241
242 let (shebang, postlude) = extract_shebang(contents)?;
244
245 let postlude = if postlude.strip_prefix('#').is_some_and(|postlude| {
247 postlude
248 .chars()
249 .next()
250 .is_some_and(|c| matches!(c, ' ' | '\r' | '\n'))
251 }) {
252 format!("\n{postlude}")
253 } else {
254 postlude
255 };
256
257 Ok((
258 if shebang.is_empty() {
259 String::new()
260 } else {
261 format!("{shebang}\n")
262 },
263 metadata,
264 postlude,
265 ))
266 }
267
268 pub async fn create(
270 file: impl AsRef<Path>,
271 requires_python: &VersionSpecifiers,
272 existing_contents: Option<Vec<u8>>,
273 bare: bool,
274 ) -> Result<(), Pep723Error> {
275 let file = file.as_ref();
276
277 let script_name = file
278 .file_name()
279 .and_then(|name| name.to_str())
280 .ok_or_else(|| Pep723Error::InvalidFilename(file.to_string_lossy().to_string()))?;
281
282 let default_metadata = indoc::formatdoc! {r#"
283 requires-python = "{requires_python}"
284 dependencies = []
285 "#,
286 };
287 let metadata = serialize_metadata(&default_metadata);
288
289 let script = if let Some(existing_contents) = existing_contents {
290 let (mut shebang, contents) = extract_shebang(&existing_contents)?;
291 if !shebang.is_empty() {
292 shebang.push_str("\n#\n");
293 if !regex::Regex::new(r"\buv\b").unwrap().is_match(&shebang) {
299 warn_user!(
300 "If you execute {} directly, it might ignore its inline metadata.\nConsider replacing its shebang with: {}",
301 file.to_string_lossy().cyan(),
302 "#!/usr/bin/env -S uv run --script".cyan(),
303 );
304 }
305 }
306 indoc::formatdoc! {r"
307 {shebang}{metadata}
308 {contents}" }
309 } else if bare {
310 metadata
311 } else {
312 indoc::formatdoc! {r#"
313 {metadata}
314
315 def main() -> None:
316 print("Hello from {name}!")
317
318
319 if __name__ == "__main__":
320 main()
321 "#,
322 metadata = metadata,
323 name = script_name,
324 }
325 };
326
327 Ok(fs_err::tokio::write(file, script).await?)
328 }
329
330 pub fn write(&self, metadata: &str) -> Result<(), io::Error> {
332 let content = format!(
333 "{}{}{}",
334 self.prelude,
335 serialize_metadata(metadata),
336 self.postlude
337 );
338
339 fs_err::write(&self.path, content)?;
340
341 Ok(())
342 }
343
344 pub fn sources(&self) -> &BTreeMap<PackageName, Sources> {
346 static EMPTY: BTreeMap<PackageName, Sources> = BTreeMap::new();
347
348 self.metadata
349 .tool
350 .as_ref()
351 .and_then(|tool| tool.uv.as_ref())
352 .and_then(|uv| uv.sources.as_ref())
353 .unwrap_or(&EMPTY)
354 }
355}
356
357#[derive(Debug, Deserialize, Clone)]
361#[serde(rename_all = "kebab-case")]
362pub struct Pep723Metadata {
363 pub dependencies: Option<Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>,
364 pub requires_python: Option<VersionSpecifiers>,
365 pub tool: Option<Tool>,
366 #[serde(skip)]
368 pub raw: String,
369}
370
371impl Pep723Metadata {
372 pub fn parse(contents: &[u8]) -> Result<Option<Self>, Pep723Error> {
374 let ScriptTag { metadata, .. } = match ScriptTag::parse(contents) {
376 Ok(Some(tag)) => tag,
377 Ok(None) => return Ok(None),
378 Err(err) => return Err(err),
379 };
380
381 Ok(Some(Self::from_str(&metadata)?))
383 }
384
385 pub async fn read(file: impl AsRef<Path>) -> Result<Option<Self>, Pep723Error> {
391 let contents = fs_err::tokio::read(&file).await?;
392
393 let ScriptTag { metadata, .. } = match ScriptTag::parse(&contents) {
395 Ok(Some(tag)) => tag,
396 Ok(None) => return Ok(None),
397 Err(err) => return Err(err),
398 };
399
400 Ok(Some(Self::from_str(&metadata)?))
402 }
403}
404
405impl FromStr for Pep723Metadata {
406 type Err = toml::de::Error;
407
408 fn from_str(raw: &str) -> Result<Self, Self::Err> {
410 let metadata = toml::from_str(raw)?;
411 Ok(Self {
412 raw: raw.to_string(),
413 ..metadata
414 })
415 }
416}
417
418#[derive(Deserialize, Debug, Clone)]
419#[serde(rename_all = "kebab-case")]
420pub struct Tool {
421 pub uv: Option<ToolUv>,
422}
423
424#[derive(Debug, Deserialize, Clone)]
425#[serde(deny_unknown_fields, rename_all = "kebab-case")]
426pub struct ToolUv {
427 #[serde(flatten)]
428 pub globals: GlobalOptions,
429 #[serde(flatten)]
430 pub top_level: ResolverInstallerSchema,
431 pub override_dependencies: Option<Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>,
432 pub exclude_dependencies: Option<Vec<uv_normalize::PackageName>>,
433 pub constraint_dependencies: Option<Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>,
434 pub build_constraint_dependencies: Option<Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>,
435 pub extra_build_dependencies: Option<BTreeMap<PackageName, Vec<ExtraBuildDependency>>>,
436 pub sources: Option<BTreeMap<PackageName, Sources>>,
437}
438
439#[derive(Debug, Error)]
440pub enum Pep723Error {
441 #[error(
442 "An opening tag (`# /// script`) was found without a closing tag (`# ///`). Ensure that every line between the opening and closing tags (including empty lines) starts with a leading `#`."
443 )]
444 UnclosedBlock,
445 #[error("The PEP 723 metadata block is missing from the script.")]
446 MissingTag,
447 #[error(transparent)]
448 Io(#[from] io::Error),
449 #[error(transparent)]
450 Utf8(#[from] std::str::Utf8Error),
451 #[error(transparent)]
452 Toml(#[from] toml::de::Error),
453 #[error("Invalid filename `{0}` supplied")]
454 InvalidFilename(String),
455}
456
457#[derive(Debug, Clone, Eq, PartialEq)]
458pub struct ScriptTag {
459 prelude: String,
461 metadata: String,
463 postlude: String,
465}
466
467impl ScriptTag {
468 pub fn parse(contents: &[u8]) -> Result<Option<Self>, Pep723Error> {
497 let Some(index) = FINDER.find(contents) else {
499 return Ok(None);
500 };
501
502 if !(index == 0 || matches!(contents[index - 1], b'\r' | b'\n')) {
504 return Ok(None);
505 }
506
507 let prelude = std::str::from_utf8(&contents[..index])?;
509
510 let contents = &contents[index..];
512 let contents = std::str::from_utf8(contents)?;
513
514 let mut lines = contents.lines();
515
516 if lines.next().is_none_or(|line| line != "# /// script") {
518 return Ok(None);
519 }
520
521 let mut toml = vec![];
527
528 for line in lines {
529 let Some(line) = line.strip_prefix('#') else {
531 break;
532 };
533
534 if line.is_empty() {
536 toml.push("");
537 continue;
538 }
539
540 let Some(line) = line.strip_prefix(' ') else {
542 break;
543 };
544
545 toml.push(line);
546 }
547
548 let Some(index) = toml.iter().rev().position(|line| *line == "///") else {
562 return Err(Pep723Error::UnclosedBlock);
563 };
564 let index = toml.len() - index;
565
566 toml.truncate(index - 1);
579
580 let prelude = prelude.to_string();
582 let metadata = toml.join("\n") + "\n";
583 let postlude = contents
584 .lines()
585 .skip(index + 1)
586 .collect::<Vec<_>>()
587 .join("\n")
588 + "\n";
589
590 Ok(Some(Self {
591 prelude,
592 metadata,
593 postlude,
594 }))
595 }
596}
597
598fn extract_shebang(contents: &[u8]) -> Result<(String, String), Pep723Error> {
601 let contents = std::str::from_utf8(contents)?;
602
603 if contents.starts_with("#!") {
604 let bytes = contents.as_bytes();
606 let index = bytes
607 .iter()
608 .position(|&b| b == b'\r' || b == b'\n')
609 .unwrap_or(bytes.len());
610
611 let width = match bytes.get(index) {
613 Some(b'\r') => {
614 if bytes.get(index + 1) == Some(&b'\n') {
615 2
616 } else {
617 1
618 }
619 }
620 Some(b'\n') => 1,
621 _ => 0,
622 };
623
624 let shebang = contents[..index].to_string();
626 let script = contents[index + width..].to_string();
627
628 Ok((shebang, script))
629 } else {
630 Ok((String::new(), contents.to_string()))
631 }
632}
633
634fn serialize_metadata(metadata: &str) -> String {
636 let mut output = String::with_capacity(metadata.len() + 32);
637
638 output.push_str("# /// script");
639 output.push('\n');
640
641 for line in metadata.lines() {
642 output.push('#');
643 if !line.is_empty() {
644 output.push(' ');
645 output.push_str(line);
646 }
647 output.push('\n');
648 }
649
650 output.push_str("# ///");
651 output.push('\n');
652
653 output
654}
655
656#[cfg(test)]
657mod tests {
658 use crate::{Pep723Error, Pep723Script, ScriptTag, serialize_metadata};
659 use std::str::FromStr;
660
661 #[test]
662 fn missing_space() {
663 let contents = indoc::indoc! {r"
664 # /// script
665 #requires-python = '>=3.11'
666 # ///
667 "};
668
669 assert!(matches!(
670 ScriptTag::parse(contents.as_bytes()),
671 Err(Pep723Error::UnclosedBlock)
672 ));
673 }
674
675 #[test]
676 fn no_closing_pragma() {
677 let contents = indoc::indoc! {r"
678 # /// script
679 # requires-python = '>=3.11'
680 # dependencies = [
681 # 'requests<3',
682 # 'rich',
683 # ]
684 "};
685
686 assert!(matches!(
687 ScriptTag::parse(contents.as_bytes()),
688 Err(Pep723Error::UnclosedBlock)
689 ));
690 }
691
692 #[test]
693 fn leading_content() {
694 let contents = indoc::indoc! {r"
695 pass # /// script
696 # requires-python = '>=3.11'
697 # dependencies = [
698 # 'requests<3',
699 # 'rich',
700 # ]
701 # ///
702 #
703 #
704 "};
705
706 assert_eq!(ScriptTag::parse(contents.as_bytes()).unwrap(), None);
707 }
708
709 #[test]
710 fn simple() {
711 let contents = indoc::indoc! {r"
712 # /// script
713 # requires-python = '>=3.11'
714 # dependencies = [
715 # 'requests<3',
716 # 'rich',
717 # ]
718 # ///
719
720 import requests
721 from rich.pretty import pprint
722
723 resp = requests.get('https://peps.python.org/api/peps.json')
724 data = resp.json()
725 "};
726
727 let expected_metadata = indoc::indoc! {r"
728 requires-python = '>=3.11'
729 dependencies = [
730 'requests<3',
731 'rich',
732 ]
733 "};
734
735 let expected_data = indoc::indoc! {r"
736
737 import requests
738 from rich.pretty import pprint
739
740 resp = requests.get('https://peps.python.org/api/peps.json')
741 data = resp.json()
742 "};
743
744 let actual = ScriptTag::parse(contents.as_bytes()).unwrap().unwrap();
745
746 assert_eq!(actual.prelude, String::new());
747 assert_eq!(actual.metadata, expected_metadata);
748 assert_eq!(actual.postlude, expected_data);
749 }
750
751 #[test]
752 fn simple_with_shebang() {
753 let contents = indoc::indoc! {r"
754 #!/usr/bin/env python3
755 # /// script
756 # requires-python = '>=3.11'
757 # dependencies = [
758 # 'requests<3',
759 # 'rich',
760 # ]
761 # ///
762
763 import requests
764 from rich.pretty import pprint
765
766 resp = requests.get('https://peps.python.org/api/peps.json')
767 data = resp.json()
768 "};
769
770 let expected_metadata = indoc::indoc! {r"
771 requires-python = '>=3.11'
772 dependencies = [
773 'requests<3',
774 'rich',
775 ]
776 "};
777
778 let expected_data = indoc::indoc! {r"
779
780 import requests
781 from rich.pretty import pprint
782
783 resp = requests.get('https://peps.python.org/api/peps.json')
784 data = resp.json()
785 "};
786
787 let actual = ScriptTag::parse(contents.as_bytes()).unwrap().unwrap();
788
789 assert_eq!(actual.prelude, "#!/usr/bin/env python3\n".to_string());
790 assert_eq!(actual.metadata, expected_metadata);
791 assert_eq!(actual.postlude, expected_data);
792 }
793
794 #[test]
795 fn embedded_comment() {
796 let contents = indoc::indoc! {r"
797 # /// script
798 # embedded-csharp = '''
799 # /// <summary>
800 # /// text
801 # ///
802 # /// </summary>
803 # public class MyClass { }
804 # '''
805 # ///
806 "};
807
808 let expected = indoc::indoc! {r"
809 embedded-csharp = '''
810 /// <summary>
811 /// text
812 ///
813 /// </summary>
814 public class MyClass { }
815 '''
816 "};
817
818 let actual = ScriptTag::parse(contents.as_bytes())
819 .unwrap()
820 .unwrap()
821 .metadata;
822
823 assert_eq!(actual, expected);
824 }
825
826 #[test]
827 fn trailing_lines() {
828 let contents = indoc::indoc! {r"
829 # /// script
830 # requires-python = '>=3.11'
831 # dependencies = [
832 # 'requests<3',
833 # 'rich',
834 # ]
835 # ///
836 #
837 #
838 "};
839
840 let expected = indoc::indoc! {r"
841 requires-python = '>=3.11'
842 dependencies = [
843 'requests<3',
844 'rich',
845 ]
846 "};
847
848 let actual = ScriptTag::parse(contents.as_bytes())
849 .unwrap()
850 .unwrap()
851 .metadata;
852
853 assert_eq!(actual, expected);
854 }
855
856 #[test]
857 fn serialize_metadata_formatting() {
858 let metadata = indoc::indoc! {r"
859 requires-python = '>=3.11'
860 dependencies = [
861 'requests<3',
862 'rich',
863 ]
864 "};
865
866 let expected_output = indoc::indoc! {r"
867 # /// script
868 # requires-python = '>=3.11'
869 # dependencies = [
870 # 'requests<3',
871 # 'rich',
872 # ]
873 # ///
874 "};
875
876 let result = serialize_metadata(metadata);
877 assert_eq!(result, expected_output);
878 }
879
880 #[test]
881 fn serialize_metadata_empty() {
882 let metadata = "";
883 let expected_output = "# /// script\n# ///\n";
884
885 let result = serialize_metadata(metadata);
886 assert_eq!(result, expected_output);
887 }
888
889 #[test]
890 fn script_init_empty() {
891 let contents = "".as_bytes();
892 let (prelude, metadata, postlude) =
893 Pep723Script::init_metadata(contents, &uv_pep440::VersionSpecifiers::default())
894 .unwrap();
895 assert_eq!(prelude, "");
896 assert_eq!(
897 metadata.raw,
898 indoc::indoc! {r"
899 dependencies = []
900 "}
901 );
902 assert_eq!(postlude, "");
903 }
904
905 #[test]
906 fn script_init_requires_python() {
907 let contents = "".as_bytes();
908 let (prelude, metadata, postlude) = Pep723Script::init_metadata(
909 contents,
910 &uv_pep440::VersionSpecifiers::from_str(">=3.8").unwrap(),
911 )
912 .unwrap();
913 assert_eq!(prelude, "");
914 assert_eq!(
915 metadata.raw,
916 indoc::indoc! {r#"
917 requires-python = ">=3.8"
918 dependencies = []
919 "#}
920 );
921 assert_eq!(postlude, "");
922 }
923
924 #[test]
925 fn script_init_with_hashbang() {
926 let contents = indoc::indoc! {r#"
927 #!/usr/bin/env python3
928
929 print("Hello, world!")
930 "#}
931 .as_bytes();
932 let (prelude, metadata, postlude) =
933 Pep723Script::init_metadata(contents, &uv_pep440::VersionSpecifiers::default())
934 .unwrap();
935 assert_eq!(prelude, "#!/usr/bin/env python3\n");
936 assert_eq!(
937 metadata.raw,
938 indoc::indoc! {r"
939 dependencies = []
940 "}
941 );
942 assert_eq!(
943 postlude,
944 indoc::indoc! {r#"
945
946 print("Hello, world!")
947 "#}
948 );
949 }
950
951 #[test]
952 fn script_init_with_other_metadata() {
953 let contents = indoc::indoc! {r#"
954 # /// noscript
955 # Hello,
956 #
957 # World!
958 # ///
959
960 print("Hello, world!")
961 "#}
962 .as_bytes();
963 let (prelude, metadata, postlude) =
964 Pep723Script::init_metadata(contents, &uv_pep440::VersionSpecifiers::default())
965 .unwrap();
966 assert_eq!(prelude, "");
967 assert_eq!(
968 metadata.raw,
969 indoc::indoc! {r"
970 dependencies = []
971 "}
972 );
973 assert_eq!(
975 postlude,
976 indoc::indoc! {r#"
977
978 # /// noscript
979 # Hello,
980 #
981 # World!
982 # ///
983
984 print("Hello, world!")
985 "#}
986 );
987 }
988
989 #[test]
990 fn script_init_with_hashbang_and_other_metadata() {
991 let contents = indoc::indoc! {r#"
992 #!/usr/bin/env python3
993 # /// noscript
994 # Hello,
995 #
996 # World!
997 # ///
998
999 print("Hello, world!")
1000 "#}
1001 .as_bytes();
1002 let (prelude, metadata, postlude) =
1003 Pep723Script::init_metadata(contents, &uv_pep440::VersionSpecifiers::default())
1004 .unwrap();
1005 assert_eq!(prelude, "#!/usr/bin/env python3\n");
1006 assert_eq!(
1007 metadata.raw,
1008 indoc::indoc! {r"
1009 dependencies = []
1010 "}
1011 );
1012 assert_eq!(
1014 postlude,
1015 indoc::indoc! {r#"
1016
1017 # /// noscript
1018 # Hello,
1019 #
1020 # World!
1021 # ///
1022
1023 print("Hello, world!")
1024 "#}
1025 );
1026 }
1027
1028 #[test]
1029 fn script_init_with_valid_metadata_line() {
1030 let contents = indoc::indoc! {r#"
1031 # Hello,
1032 # /// noscript
1033 #
1034 # World!
1035 # ///
1036
1037 print("Hello, world!")
1038 "#}
1039 .as_bytes();
1040 let (prelude, metadata, postlude) =
1041 Pep723Script::init_metadata(contents, &uv_pep440::VersionSpecifiers::default())
1042 .unwrap();
1043 assert_eq!(prelude, "");
1044 assert_eq!(
1045 metadata.raw,
1046 indoc::indoc! {r"
1047 dependencies = []
1048 "}
1049 );
1050 assert_eq!(
1052 postlude,
1053 indoc::indoc! {r#"
1054
1055 # Hello,
1056 # /// noscript
1057 #
1058 # World!
1059 # ///
1060
1061 print("Hello, world!")
1062 "#}
1063 );
1064 }
1065
1066 #[test]
1067 fn script_init_with_valid_empty_metadata_line() {
1068 let contents = indoc::indoc! {r#"
1069 #
1070 # /// noscript
1071 # Hello,
1072 # World!
1073 # ///
1074
1075 print("Hello, world!")
1076 "#}
1077 .as_bytes();
1078 let (prelude, metadata, postlude) =
1079 Pep723Script::init_metadata(contents, &uv_pep440::VersionSpecifiers::default())
1080 .unwrap();
1081 assert_eq!(prelude, "");
1082 assert_eq!(
1083 metadata.raw,
1084 indoc::indoc! {r"
1085 dependencies = []
1086 "}
1087 );
1088 assert_eq!(
1090 postlude,
1091 indoc::indoc! {r#"
1092
1093 #
1094 # /// noscript
1095 # Hello,
1096 # World!
1097 # ///
1098
1099 print("Hello, world!")
1100 "#}
1101 );
1102 }
1103
1104 #[test]
1105 fn script_init_with_non_metadata_comment() {
1106 let contents = indoc::indoc! {r#"
1107 #Hello,
1108 # /// noscript
1109 #
1110 # World!
1111 # ///
1112
1113 print("Hello, world!")
1114 "#}
1115 .as_bytes();
1116 let (prelude, metadata, postlude) =
1117 Pep723Script::init_metadata(contents, &uv_pep440::VersionSpecifiers::default())
1118 .unwrap();
1119 assert_eq!(prelude, "");
1120 assert_eq!(
1121 metadata.raw,
1122 indoc::indoc! {r"
1123 dependencies = []
1124 "}
1125 );
1126 assert_eq!(
1127 postlude,
1128 indoc::indoc! {r#"
1129 #Hello,
1130 # /// noscript
1131 #
1132 # World!
1133 # ///
1134
1135 print("Hello, world!")
1136 "#}
1137 );
1138 }
1139}