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::SourceStrategy;
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: SourceStrategy) -> &[uv_distribution_types::Index] {
114 match source_strategy {
115 SourceStrategy::Enabled => 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 SourceStrategy::Disabled => &[],
123 }
124 }
125
126 pub fn sources(&self, source_strategy: SourceStrategy) -> &BTreeMap<PackageName, Sources> {
128 static EMPTY: BTreeMap<PackageName, Sources> = BTreeMap::new();
129 match source_strategy {
130 SourceStrategy::Enabled => 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 SourceStrategy::Disabled => &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 ) -> Result<(), Pep723Error> {
274 let file = file.as_ref();
275
276 let script_name = file
277 .file_name()
278 .and_then(|name| name.to_str())
279 .ok_or_else(|| Pep723Error::InvalidFilename(file.to_string_lossy().to_string()))?;
280
281 let default_metadata = indoc::formatdoc! {r#"
282 requires-python = "{requires_python}"
283 dependencies = []
284 "#,
285 };
286 let metadata = serialize_metadata(&default_metadata);
287
288 let script = if let Some(existing_contents) = existing_contents {
289 let (mut shebang, contents) = extract_shebang(&existing_contents)?;
290 if !shebang.is_empty() {
291 shebang.push_str("\n#\n");
292 if !regex::Regex::new(r"\buv\b").unwrap().is_match(&shebang) {
298 warn_user!(
299 "If you execute {} directly, it might ignore its inline metadata.\nConsider replacing its shebang with: {}",
300 file.to_string_lossy().cyan(),
301 "#!/usr/bin/env -S uv run --script".cyan(),
302 );
303 }
304 }
305 indoc::formatdoc! {r"
306 {shebang}{metadata}
307 {contents}" }
308 } else {
309 indoc::formatdoc! {r#"
310 {metadata}
311
312 def main() -> None:
313 print("Hello from {name}!")
314
315
316 if __name__ == "__main__":
317 main()
318 "#,
319 metadata = metadata,
320 name = script_name,
321 }
322 };
323
324 Ok(fs_err::tokio::write(file, script).await?)
325 }
326
327 pub fn write(&self, metadata: &str) -> Result<(), io::Error> {
329 let content = format!(
330 "{}{}{}",
331 self.prelude,
332 serialize_metadata(metadata),
333 self.postlude
334 );
335
336 fs_err::write(&self.path, content)?;
337
338 Ok(())
339 }
340
341 pub fn sources(&self) -> &BTreeMap<PackageName, Sources> {
343 static EMPTY: BTreeMap<PackageName, Sources> = BTreeMap::new();
344
345 self.metadata
346 .tool
347 .as_ref()
348 .and_then(|tool| tool.uv.as_ref())
349 .and_then(|uv| uv.sources.as_ref())
350 .unwrap_or(&EMPTY)
351 }
352}
353
354#[derive(Debug, Deserialize, Clone)]
358#[serde(rename_all = "kebab-case")]
359pub struct Pep723Metadata {
360 pub dependencies: Option<Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>,
361 pub requires_python: Option<VersionSpecifiers>,
362 pub tool: Option<Tool>,
363 #[serde(skip)]
365 pub raw: String,
366}
367
368impl Pep723Metadata {
369 pub fn parse(contents: &[u8]) -> Result<Option<Self>, Pep723Error> {
371 let ScriptTag { metadata, .. } = match ScriptTag::parse(contents) {
373 Ok(Some(tag)) => tag,
374 Ok(None) => return Ok(None),
375 Err(err) => return Err(err),
376 };
377
378 Ok(Some(Self::from_str(&metadata)?))
380 }
381
382 pub async fn read(file: impl AsRef<Path>) -> Result<Option<Self>, Pep723Error> {
388 let contents = fs_err::tokio::read(&file).await?;
389
390 let ScriptTag { metadata, .. } = match ScriptTag::parse(&contents) {
392 Ok(Some(tag)) => tag,
393 Ok(None) => return Ok(None),
394 Err(err) => return Err(err),
395 };
396
397 Ok(Some(Self::from_str(&metadata)?))
399 }
400}
401
402impl FromStr for Pep723Metadata {
403 type Err = toml::de::Error;
404
405 fn from_str(raw: &str) -> Result<Self, Self::Err> {
407 let metadata = toml::from_str(raw)?;
408 Ok(Self {
409 raw: raw.to_string(),
410 ..metadata
411 })
412 }
413}
414
415#[derive(Deserialize, Debug, Clone)]
416#[serde(rename_all = "kebab-case")]
417pub struct Tool {
418 pub uv: Option<ToolUv>,
419}
420
421#[derive(Debug, Deserialize, Clone)]
422#[serde(deny_unknown_fields, rename_all = "kebab-case")]
423pub struct ToolUv {
424 #[serde(flatten)]
425 pub globals: GlobalOptions,
426 #[serde(flatten)]
427 pub top_level: ResolverInstallerSchema,
428 pub override_dependencies: Option<Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>,
429 pub exclude_dependencies: Option<Vec<uv_normalize::PackageName>>,
430 pub constraint_dependencies: Option<Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>,
431 pub build_constraint_dependencies: Option<Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>,
432 pub extra_build_dependencies: Option<BTreeMap<PackageName, Vec<ExtraBuildDependency>>>,
433 pub sources: Option<BTreeMap<PackageName, Sources>>,
434}
435
436#[derive(Debug, Error)]
437pub enum Pep723Error {
438 #[error(
439 "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 `#`."
440 )]
441 UnclosedBlock,
442 #[error("The PEP 723 metadata block is missing from the script.")]
443 MissingTag,
444 #[error(transparent)]
445 Io(#[from] io::Error),
446 #[error(transparent)]
447 Utf8(#[from] std::str::Utf8Error),
448 #[error(transparent)]
449 Toml(#[from] toml::de::Error),
450 #[error("Invalid filename `{0}` supplied")]
451 InvalidFilename(String),
452}
453
454#[derive(Debug, Clone, Eq, PartialEq)]
455pub struct ScriptTag {
456 prelude: String,
458 metadata: String,
460 postlude: String,
462}
463
464impl ScriptTag {
465 pub fn parse(contents: &[u8]) -> Result<Option<Self>, Pep723Error> {
494 let Some(index) = FINDER.find(contents) else {
496 return Ok(None);
497 };
498
499 if !(index == 0 || matches!(contents[index - 1], b'\r' | b'\n')) {
501 return Ok(None);
502 }
503
504 let prelude = std::str::from_utf8(&contents[..index])?;
506
507 let contents = &contents[index..];
509 let contents = std::str::from_utf8(contents)?;
510
511 let mut lines = contents.lines();
512
513 if lines.next().is_none_or(|line| line != "# /// script") {
515 return Ok(None);
516 }
517
518 let mut toml = vec![];
524
525 for line in lines {
526 let Some(line) = line.strip_prefix('#') else {
528 break;
529 };
530
531 if line.is_empty() {
533 toml.push("");
534 continue;
535 }
536
537 let Some(line) = line.strip_prefix(' ') else {
539 break;
540 };
541
542 toml.push(line);
543 }
544
545 let Some(index) = toml.iter().rev().position(|line| *line == "///") else {
559 return Err(Pep723Error::UnclosedBlock);
560 };
561 let index = toml.len() - index;
562
563 toml.truncate(index - 1);
576
577 let prelude = prelude.to_string();
579 let metadata = toml.join("\n") + "\n";
580 let postlude = contents
581 .lines()
582 .skip(index + 1)
583 .collect::<Vec<_>>()
584 .join("\n")
585 + "\n";
586
587 Ok(Some(Self {
588 prelude,
589 metadata,
590 postlude,
591 }))
592 }
593}
594
595fn extract_shebang(contents: &[u8]) -> Result<(String, String), Pep723Error> {
598 let contents = std::str::from_utf8(contents)?;
599
600 if contents.starts_with("#!") {
601 let bytes = contents.as_bytes();
603 let index = bytes
604 .iter()
605 .position(|&b| b == b'\r' || b == b'\n')
606 .unwrap_or(bytes.len());
607
608 let width = match bytes.get(index) {
610 Some(b'\r') => {
611 if bytes.get(index + 1) == Some(&b'\n') {
612 2
613 } else {
614 1
615 }
616 }
617 Some(b'\n') => 1,
618 _ => 0,
619 };
620
621 let shebang = contents[..index].to_string();
623 let script = contents[index + width..].to_string();
624
625 Ok((shebang, script))
626 } else {
627 Ok((String::new(), contents.to_string()))
628 }
629}
630
631fn serialize_metadata(metadata: &str) -> String {
633 let mut output = String::with_capacity(metadata.len() + 32);
634
635 output.push_str("# /// script");
636 output.push('\n');
637
638 for line in metadata.lines() {
639 output.push('#');
640 if !line.is_empty() {
641 output.push(' ');
642 output.push_str(line);
643 }
644 output.push('\n');
645 }
646
647 output.push_str("# ///");
648 output.push('\n');
649
650 output
651}
652
653#[cfg(test)]
654mod tests {
655 use crate::{Pep723Error, Pep723Script, ScriptTag, serialize_metadata};
656 use std::str::FromStr;
657
658 #[test]
659 fn missing_space() {
660 let contents = indoc::indoc! {r"
661 # /// script
662 #requires-python = '>=3.11'
663 # ///
664 "};
665
666 assert!(matches!(
667 ScriptTag::parse(contents.as_bytes()),
668 Err(Pep723Error::UnclosedBlock)
669 ));
670 }
671
672 #[test]
673 fn no_closing_pragma() {
674 let contents = indoc::indoc! {r"
675 # /// script
676 # requires-python = '>=3.11'
677 # dependencies = [
678 # 'requests<3',
679 # 'rich',
680 # ]
681 "};
682
683 assert!(matches!(
684 ScriptTag::parse(contents.as_bytes()),
685 Err(Pep723Error::UnclosedBlock)
686 ));
687 }
688
689 #[test]
690 fn leading_content() {
691 let contents = indoc::indoc! {r"
692 pass # /// script
693 # requires-python = '>=3.11'
694 # dependencies = [
695 # 'requests<3',
696 # 'rich',
697 # ]
698 # ///
699 #
700 #
701 "};
702
703 assert_eq!(ScriptTag::parse(contents.as_bytes()).unwrap(), None);
704 }
705
706 #[test]
707 fn simple() {
708 let contents = indoc::indoc! {r"
709 # /// script
710 # requires-python = '>=3.11'
711 # dependencies = [
712 # 'requests<3',
713 # 'rich',
714 # ]
715 # ///
716
717 import requests
718 from rich.pretty import pprint
719
720 resp = requests.get('https://peps.python.org/api/peps.json')
721 data = resp.json()
722 "};
723
724 let expected_metadata = indoc::indoc! {r"
725 requires-python = '>=3.11'
726 dependencies = [
727 'requests<3',
728 'rich',
729 ]
730 "};
731
732 let expected_data = indoc::indoc! {r"
733
734 import requests
735 from rich.pretty import pprint
736
737 resp = requests.get('https://peps.python.org/api/peps.json')
738 data = resp.json()
739 "};
740
741 let actual = ScriptTag::parse(contents.as_bytes()).unwrap().unwrap();
742
743 assert_eq!(actual.prelude, String::new());
744 assert_eq!(actual.metadata, expected_metadata);
745 assert_eq!(actual.postlude, expected_data);
746 }
747
748 #[test]
749 fn simple_with_shebang() {
750 let contents = indoc::indoc! {r"
751 #!/usr/bin/env python3
752 # /// script
753 # requires-python = '>=3.11'
754 # dependencies = [
755 # 'requests<3',
756 # 'rich',
757 # ]
758 # ///
759
760 import requests
761 from rich.pretty import pprint
762
763 resp = requests.get('https://peps.python.org/api/peps.json')
764 data = resp.json()
765 "};
766
767 let expected_metadata = indoc::indoc! {r"
768 requires-python = '>=3.11'
769 dependencies = [
770 'requests<3',
771 'rich',
772 ]
773 "};
774
775 let expected_data = indoc::indoc! {r"
776
777 import requests
778 from rich.pretty import pprint
779
780 resp = requests.get('https://peps.python.org/api/peps.json')
781 data = resp.json()
782 "};
783
784 let actual = ScriptTag::parse(contents.as_bytes()).unwrap().unwrap();
785
786 assert_eq!(actual.prelude, "#!/usr/bin/env python3\n".to_string());
787 assert_eq!(actual.metadata, expected_metadata);
788 assert_eq!(actual.postlude, expected_data);
789 }
790
791 #[test]
792 fn embedded_comment() {
793 let contents = indoc::indoc! {r"
794 # /// script
795 # embedded-csharp = '''
796 # /// <summary>
797 # /// text
798 # ///
799 # /// </summary>
800 # public class MyClass { }
801 # '''
802 # ///
803 "};
804
805 let expected = indoc::indoc! {r"
806 embedded-csharp = '''
807 /// <summary>
808 /// text
809 ///
810 /// </summary>
811 public class MyClass { }
812 '''
813 "};
814
815 let actual = ScriptTag::parse(contents.as_bytes())
816 .unwrap()
817 .unwrap()
818 .metadata;
819
820 assert_eq!(actual, expected);
821 }
822
823 #[test]
824 fn trailing_lines() {
825 let contents = indoc::indoc! {r"
826 # /// script
827 # requires-python = '>=3.11'
828 # dependencies = [
829 # 'requests<3',
830 # 'rich',
831 # ]
832 # ///
833 #
834 #
835 "};
836
837 let expected = indoc::indoc! {r"
838 requires-python = '>=3.11'
839 dependencies = [
840 'requests<3',
841 'rich',
842 ]
843 "};
844
845 let actual = ScriptTag::parse(contents.as_bytes())
846 .unwrap()
847 .unwrap()
848 .metadata;
849
850 assert_eq!(actual, expected);
851 }
852
853 #[test]
854 fn serialize_metadata_formatting() {
855 let metadata = indoc::indoc! {r"
856 requires-python = '>=3.11'
857 dependencies = [
858 'requests<3',
859 'rich',
860 ]
861 "};
862
863 let expected_output = indoc::indoc! {r"
864 # /// script
865 # requires-python = '>=3.11'
866 # dependencies = [
867 # 'requests<3',
868 # 'rich',
869 # ]
870 # ///
871 "};
872
873 let result = serialize_metadata(metadata);
874 assert_eq!(result, expected_output);
875 }
876
877 #[test]
878 fn serialize_metadata_empty() {
879 let metadata = "";
880 let expected_output = "# /// script\n# ///\n";
881
882 let result = serialize_metadata(metadata);
883 assert_eq!(result, expected_output);
884 }
885
886 #[test]
887 fn script_init_empty() {
888 let contents = "".as_bytes();
889 let (prelude, metadata, postlude) =
890 Pep723Script::init_metadata(contents, &uv_pep440::VersionSpecifiers::default())
891 .unwrap();
892 assert_eq!(prelude, "");
893 assert_eq!(
894 metadata.raw,
895 indoc::indoc! {r"
896 dependencies = []
897 "}
898 );
899 assert_eq!(postlude, "");
900 }
901
902 #[test]
903 fn script_init_requires_python() {
904 let contents = "".as_bytes();
905 let (prelude, metadata, postlude) = Pep723Script::init_metadata(
906 contents,
907 &uv_pep440::VersionSpecifiers::from_str(">=3.8").unwrap(),
908 )
909 .unwrap();
910 assert_eq!(prelude, "");
911 assert_eq!(
912 metadata.raw,
913 indoc::indoc! {r#"
914 requires-python = ">=3.8"
915 dependencies = []
916 "#}
917 );
918 assert_eq!(postlude, "");
919 }
920
921 #[test]
922 fn script_init_with_hashbang() {
923 let contents = indoc::indoc! {r#"
924 #!/usr/bin/env python3
925
926 print("Hello, world!")
927 "#}
928 .as_bytes();
929 let (prelude, metadata, postlude) =
930 Pep723Script::init_metadata(contents, &uv_pep440::VersionSpecifiers::default())
931 .unwrap();
932 assert_eq!(prelude, "#!/usr/bin/env python3\n");
933 assert_eq!(
934 metadata.raw,
935 indoc::indoc! {r"
936 dependencies = []
937 "}
938 );
939 assert_eq!(
940 postlude,
941 indoc::indoc! {r#"
942
943 print("Hello, world!")
944 "#}
945 );
946 }
947
948 #[test]
949 fn script_init_with_other_metadata() {
950 let contents = indoc::indoc! {r#"
951 # /// noscript
952 # Hello,
953 #
954 # World!
955 # ///
956
957 print("Hello, world!")
958 "#}
959 .as_bytes();
960 let (prelude, metadata, postlude) =
961 Pep723Script::init_metadata(contents, &uv_pep440::VersionSpecifiers::default())
962 .unwrap();
963 assert_eq!(prelude, "");
964 assert_eq!(
965 metadata.raw,
966 indoc::indoc! {r"
967 dependencies = []
968 "}
969 );
970 assert_eq!(
972 postlude,
973 indoc::indoc! {r#"
974
975 # /// noscript
976 # Hello,
977 #
978 # World!
979 # ///
980
981 print("Hello, world!")
982 "#}
983 );
984 }
985
986 #[test]
987 fn script_init_with_hashbang_and_other_metadata() {
988 let contents = indoc::indoc! {r#"
989 #!/usr/bin/env python3
990 # /// noscript
991 # Hello,
992 #
993 # World!
994 # ///
995
996 print("Hello, world!")
997 "#}
998 .as_bytes();
999 let (prelude, metadata, postlude) =
1000 Pep723Script::init_metadata(contents, &uv_pep440::VersionSpecifiers::default())
1001 .unwrap();
1002 assert_eq!(prelude, "#!/usr/bin/env python3\n");
1003 assert_eq!(
1004 metadata.raw,
1005 indoc::indoc! {r"
1006 dependencies = []
1007 "}
1008 );
1009 assert_eq!(
1011 postlude,
1012 indoc::indoc! {r#"
1013
1014 # /// noscript
1015 # Hello,
1016 #
1017 # World!
1018 # ///
1019
1020 print("Hello, world!")
1021 "#}
1022 );
1023 }
1024
1025 #[test]
1026 fn script_init_with_valid_metadata_line() {
1027 let contents = indoc::indoc! {r#"
1028 # Hello,
1029 # /// noscript
1030 #
1031 # World!
1032 # ///
1033
1034 print("Hello, world!")
1035 "#}
1036 .as_bytes();
1037 let (prelude, metadata, postlude) =
1038 Pep723Script::init_metadata(contents, &uv_pep440::VersionSpecifiers::default())
1039 .unwrap();
1040 assert_eq!(prelude, "");
1041 assert_eq!(
1042 metadata.raw,
1043 indoc::indoc! {r"
1044 dependencies = []
1045 "}
1046 );
1047 assert_eq!(
1049 postlude,
1050 indoc::indoc! {r#"
1051
1052 # Hello,
1053 # /// noscript
1054 #
1055 # World!
1056 # ///
1057
1058 print("Hello, world!")
1059 "#}
1060 );
1061 }
1062
1063 #[test]
1064 fn script_init_with_valid_empty_metadata_line() {
1065 let contents = indoc::indoc! {r#"
1066 #
1067 # /// noscript
1068 # Hello,
1069 # World!
1070 # ///
1071
1072 print("Hello, world!")
1073 "#}
1074 .as_bytes();
1075 let (prelude, metadata, postlude) =
1076 Pep723Script::init_metadata(contents, &uv_pep440::VersionSpecifiers::default())
1077 .unwrap();
1078 assert_eq!(prelude, "");
1079 assert_eq!(
1080 metadata.raw,
1081 indoc::indoc! {r"
1082 dependencies = []
1083 "}
1084 );
1085 assert_eq!(
1087 postlude,
1088 indoc::indoc! {r#"
1089
1090 #
1091 # /// noscript
1092 # Hello,
1093 # World!
1094 # ///
1095
1096 print("Hello, world!")
1097 "#}
1098 );
1099 }
1100
1101 #[test]
1102 fn script_init_with_non_metadata_comment() {
1103 let contents = indoc::indoc! {r#"
1104 #Hello,
1105 # /// noscript
1106 #
1107 # World!
1108 # ///
1109
1110 print("Hello, world!")
1111 "#}
1112 .as_bytes();
1113 let (prelude, metadata, postlude) =
1114 Pep723Script::init_metadata(contents, &uv_pep440::VersionSpecifiers::default())
1115 .unwrap();
1116 assert_eq!(prelude, "");
1117 assert_eq!(
1118 metadata.raw,
1119 indoc::indoc! {r"
1120 dependencies = []
1121 "}
1122 );
1123 assert_eq!(
1124 postlude,
1125 indoc::indoc! {r#"
1126 #Hello,
1127 # /// noscript
1128 #
1129 # World!
1130 # ///
1131
1132 print("Hello, world!")
1133 "#}
1134 );
1135 }
1136}