1use std::borrow::Cow;
2use std::collections::BTreeMap;
3use std::io;
4use std::path::{Path, PathBuf};
5use std::str::FromStr;
6use std::sync::LazyLock;
7
8use memchr::memmem::Finder;
9use serde::Deserialize;
10use thiserror::Error;
11use tracing::instrument;
12use url::Url;
13
14use uv_configuration::NoSources;
15use uv_normalize::PackageName;
16use uv_pep440::VersionSpecifiers;
17use uv_pypi_types::VerbatimParsedUrl;
18use uv_redacted::DisplaySafeUrl;
19use uv_settings::{GlobalOptions, ResolverInstallerSchema};
20use uv_warnings::warn_user;
21use uv_workspace::pyproject::{ExtraBuildDependency, Sources};
22
23pub use uv_configuration::ExcludeDependency;
24pub use uv_workspace::pyproject::OverrideDependency;
25
26static FINDER: LazyLock<Finder> = LazyLock::new(|| Finder::new(b"# /// script"));
27
28#[derive(Debug)]
30pub enum Pep723Item {
31 Script(Pep723Script),
33 Stdin(Pep723Metadata),
35 Remote(Pep723Metadata, DisplaySafeUrl),
37}
38
39impl Pep723Item {
40 pub fn metadata(&self) -> &Pep723Metadata {
42 match self {
43 Self::Script(script) => &script.metadata,
44 Self::Stdin(metadata) => metadata,
45 Self::Remote(metadata, ..) => metadata,
46 }
47 }
48
49 pub fn as_script(&self) -> Option<&Pep723Script> {
51 match self {
52 Self::Script(script) => Some(script),
53 _ => None,
54 }
55 }
56}
57
58#[derive(Debug, Copy, Clone)]
60pub enum Pep723ItemRef<'item> {
61 Script(&'item Pep723Script),
63 Stdin(&'item Pep723Metadata),
65 Remote(&'item Pep723Metadata, &'item Url),
67}
68
69impl Pep723ItemRef<'_> {
70 pub fn metadata(&self) -> &Pep723Metadata {
72 match self {
73 Self::Script(script) => &script.metadata,
74 Self::Stdin(metadata) => metadata,
75 Self::Remote(metadata, ..) => metadata,
76 }
77 }
78
79 pub fn path(&self) -> Option<&Path> {
81 match self {
82 Self::Script(script) => Some(&script.path),
83 Self::Stdin(..) => None,
84 Self::Remote(..) => None,
85 }
86 }
87
88 pub fn directory(&self) -> Result<PathBuf, io::Error> {
90 match self {
91 Self::Script(script) => Ok(std::path::absolute(&script.path)?
92 .parent()
93 .expect("script path has no parent")
94 .to_owned()),
95 Self::Stdin(..) | Self::Remote(..) => std::env::current_dir(),
96 }
97 }
98
99 pub fn indexes(&self, source_strategy: &NoSources) -> &[uv_distribution_types::Index] {
101 match source_strategy {
102 NoSources::None | NoSources::Packages(_) => self
103 .metadata()
104 .tool
105 .as_ref()
106 .and_then(|tool| tool.uv.as_ref())
107 .and_then(|uv| uv.top_level.index.as_deref())
108 .unwrap_or(&[]),
109 NoSources::All => &[],
110 }
111 }
112
113 pub fn sources(&self, source_strategy: &NoSources) -> Cow<'_, BTreeMap<PackageName, Sources>> {
115 static EMPTY: BTreeMap<PackageName, Sources> = BTreeMap::new();
116 let sources = self
117 .metadata()
118 .tool
119 .as_ref()
120 .and_then(|tool| tool.uv.as_ref())
121 .and_then(|uv| uv.sources.as_ref())
122 .unwrap_or(&EMPTY);
123
124 match source_strategy {
125 NoSources::None => Cow::Borrowed(sources),
126 NoSources::All => Cow::Borrowed(&EMPTY),
127 NoSources::Packages(packages) => Cow::Owned(
128 sources
129 .iter()
130 .filter(|(name, _)| !packages.contains(name))
131 .map(|(name, sources)| (name.clone(), sources.clone()))
132 .collect(),
133 ),
134 }
135 }
136}
137
138impl<'item> From<&'item Pep723Item> for Pep723ItemRef<'item> {
139 fn from(item: &'item Pep723Item) -> Self {
140 match item {
141 Pep723Item::Script(script) => Self::Script(script),
142 Pep723Item::Stdin(metadata) => Self::Stdin(metadata),
143 Pep723Item::Remote(metadata, url) => Self::Remote(metadata, url),
144 }
145 }
146}
147
148impl<'item> From<&'item Pep723Script> for Pep723ItemRef<'item> {
149 fn from(script: &'item Pep723Script) -> Self {
150 Self::Script(script)
151 }
152}
153
154#[derive(Debug, Clone)]
156pub struct Pep723Script {
157 pub path: PathBuf,
159 pub metadata: Pep723Metadata,
161 pub prelude: String,
163 pub postlude: String,
165}
166
167impl Pep723Script {
168 pub async fn read(file: impl AsRef<Path>) -> Result<Option<Self>, Pep723Error> {
174 let contents = fs_err::tokio::read(&file).await?;
175
176 let ScriptTag {
178 prelude,
179 metadata,
180 postlude,
181 } = match ScriptTag::parse(&contents) {
182 Ok(Some(tag)) => tag,
183 Ok(None) => return Ok(None),
184 Err(err) => return Err(err),
185 };
186
187 let metadata = Pep723Metadata::from_str(&metadata)?;
189
190 Ok(Some(Self {
191 path: std::path::absolute(file)?,
192 metadata,
193 prelude,
194 postlude,
195 }))
196 }
197
198 pub async fn init(
202 file: impl AsRef<Path>,
203 requires_python: &VersionSpecifiers,
204 ) -> Result<Self, Pep723Error> {
205 let contents = fs_err::tokio::read(&file).await?;
206 let (prelude, metadata, postlude) = Self::init_metadata(&contents, requires_python)?;
207 Ok(Self {
208 path: std::path::absolute(file)?,
209 metadata,
210 prelude,
211 postlude,
212 })
213 }
214
215 fn init_metadata(
219 contents: &[u8],
220 requires_python: &VersionSpecifiers,
221 ) -> Result<(String, Pep723Metadata, String), Pep723Error> {
222 let default_metadata = if requires_python.is_empty() {
224 indoc::formatdoc! {r"
225 dependencies = []
226 ",
227 }
228 } else {
229 indoc::formatdoc! {r#"
230 requires-python = "{requires_python}"
231 dependencies = []
232 "#,
233 requires_python = requires_python,
234 }
235 };
236 let metadata = Pep723Metadata::from_str(&default_metadata)?;
237
238 let (shebang, postlude) = extract_shebang(contents)?;
240
241 let postlude = if postlude.strip_prefix('#').is_some_and(|postlude| {
243 postlude
244 .chars()
245 .next()
246 .is_some_and(|c| matches!(c, ' ' | '\r' | '\n'))
247 }) {
248 format!("\n{postlude}")
249 } else {
250 postlude
251 };
252
253 Ok((
254 if shebang.is_empty() {
255 String::new()
256 } else {
257 format!("{shebang}\n")
258 },
259 metadata,
260 postlude,
261 ))
262 }
263
264 pub async fn create(
266 file: impl AsRef<Path>,
267 requires_python: &VersionSpecifiers,
268 existing_contents: Option<Vec<u8>>,
269 bare: bool,
270 ) -> Result<(), Pep723Error> {
271 let file = file.as_ref();
272
273 let script_name = file
274 .file_name()
275 .and_then(|name| name.to_str())
276 .ok_or_else(|| Pep723Error::InvalidFilename(file.to_string_lossy().to_string()))?;
277
278 let default_metadata = indoc::formatdoc! {r#"
279 requires-python = "{requires_python}"
280 dependencies = []
281 "#,
282 };
283 let metadata = serialize_metadata(&default_metadata);
284
285 let script = if let Some(existing_contents) = existing_contents {
286 let (mut shebang, contents) = extract_shebang(&existing_contents)?;
287 if !shebang.is_empty() {
288 shebang.push_str("\n#\n");
289 if !regex::Regex::new(r"\buv\b").unwrap().is_match(&shebang) {
295 warn_user!(
296 "If you execute {} directly, it might ignore its inline metadata.\nConsider replacing its shebang with: {}",
297 file.to_string_lossy().cyan(),
298 "#!/usr/bin/env -S uv run --script".cyan(),
299 );
300 }
301 }
302 indoc::formatdoc! {r"
303 {shebang}{metadata}
304 {contents}" }
305 } else if bare {
306 metadata
307 } else {
308 indoc::formatdoc! {r#"
309 {metadata}
310
311 def main() -> None:
312 print("Hello from {name}!")
313
314
315 if __name__ == "__main__":
316 main()
317 "#,
318 metadata = metadata,
319 name = script_name,
320 }
321 };
322
323 Ok(fs_err::tokio::write(file, script).await?)
324 }
325
326 pub fn write(&self, metadata: &str) -> Result<(), io::Error> {
328 let content = format!(
329 "{}{}{}",
330 self.prelude,
331 serialize_metadata(metadata),
332 self.postlude
333 );
334
335 fs_err::write(&self.path, content)?;
336
337 Ok(())
338 }
339
340 pub fn sources(&self) -> &BTreeMap<PackageName, Sources> {
342 static EMPTY: BTreeMap<PackageName, Sources> = BTreeMap::new();
343
344 self.metadata
345 .tool
346 .as_ref()
347 .and_then(|tool| tool.uv.as_ref())
348 .and_then(|uv| uv.sources.as_ref())
349 .unwrap_or(&EMPTY)
350 }
351}
352
353#[derive(Debug, Deserialize, Clone)]
357#[serde(rename_all = "kebab-case")]
358pub struct Pep723Metadata {
359 pub dependencies: Option<Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>,
360 pub requires_python: Option<VersionSpecifiers>,
361 pub tool: Option<Tool>,
362 #[serde(skip)]
364 pub raw: String,
365}
366
367impl Pep723Metadata {
368 pub fn parse(contents: &[u8]) -> Result<Option<Self>, Pep723Error> {
370 let ScriptTag { metadata, .. } = match ScriptTag::parse(contents) {
372 Ok(Some(tag)) => tag,
373 Ok(None) => return Ok(None),
374 Err(err) => return Err(err),
375 };
376
377 Ok(Some(Self::from_str(&metadata)?))
379 }
380
381 pub async fn read(file: impl AsRef<Path>) -> Result<Option<Self>, Pep723Error> {
387 let contents = fs_err::tokio::read(&file).await?;
388
389 let ScriptTag { metadata, .. } = match ScriptTag::parse(&contents) {
391 Ok(Some(tag)) => tag,
392 Ok(None) => return Ok(None),
393 Err(err) => return Err(err),
394 };
395
396 Ok(Some(Self::from_str(&metadata)?))
398 }
399}
400
401impl FromStr for Pep723Metadata {
402 type Err = toml::de::Error;
403
404 #[instrument(name = "toml::from_str PEP 723 metadata", skip_all)]
406 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<OverrideDependency>>,
429 pub exclude_dependencies: Option<Vec<ExcludeDependency>>,
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 script contains multiple PEP 723 metadata blocks")]
443 DuplicateBlock,
444 #[error("The PEP 723 metadata block is missing from the script.")]
445 MissingTag,
446 #[error(transparent)]
447 Io(#[from] io::Error),
448 #[error(transparent)]
449 Utf8(#[from] std::str::Utf8Error),
450 #[error(transparent)]
451 Toml(#[from] toml::de::Error),
452 #[error("Invalid filename `{0}` supplied")]
453 InvalidFilename(String),
454}
455
456#[derive(Debug, Clone, Eq, PartialEq)]
457pub struct ScriptTag {
458 prelude: String,
460 metadata: String,
462 postlude: String,
464}
465
466impl ScriptTag {
467 pub fn parse(contents: &[u8]) -> Result<Option<Self>, Pep723Error> {
496 let Some(index) = FINDER.find(contents) else {
498 return Ok(None);
499 };
500
501 if !(index == 0 || matches!(contents[index - 1], b'\r' | b'\n')) {
503 return Ok(None);
504 }
505
506 let prelude = std::str::from_utf8(&contents[..index])?;
508
509 let contents = &contents[index..];
511 let contents = std::str::from_utf8(contents)?;
512
513 let mut lines = contents.lines();
514
515 if lines.next().is_none_or(|line| line != "# /// script") {
517 return Ok(None);
518 }
519
520 let mut toml = vec![];
526
527 for line in lines {
528 let Some(line) = line.strip_prefix('#') else {
530 break;
531 };
532
533 if line.is_empty() {
535 toml.push("");
536 continue;
537 }
538
539 let Some(line) = line.strip_prefix(' ') else {
541 break;
542 };
543
544 toml.push(line);
545 }
546
547 let Some(index) = toml.iter().rev().position(|line| *line == "///") else {
561 return Err(Pep723Error::UnclosedBlock);
562 };
563 let index = toml.len() - index;
564
565 toml.truncate(index - 1);
578
579 let postlude = contents.lines().skip(index + 1).collect::<Vec<_>>();
581
582 let mut lines = postlude.iter().peekable();
585 while let Some(line) = lines.next() {
586 let Some(metadata_type) = line.strip_prefix("# /// ") else {
588 continue;
589 };
590
591 if metadata_type.is_empty()
593 || !metadata_type
594 .bytes()
595 .all(|byte| byte.is_ascii_alphanumeric() || byte == b'-')
596 {
597 continue;
598 }
599
600 let is_script_block = metadata_type == "script";
601 let mut is_closed = false;
602 while let Some(line) = lines.next() {
603 let Some(content) = line.strip_prefix('#') else {
605 break;
606 };
607 if !(content.is_empty() || content.starts_with(' ')) {
608 break;
609 }
610
611 if *line == "# ///" {
612 let Some(next_line) = lines.peek() else {
613 is_closed = true;
614 break;
615 };
616
617 let Some(next_content) = next_line.strip_prefix('#') else {
618 is_closed = true;
619 break;
620 };
621
622 if !(next_content.is_empty() || next_content.starts_with(' ')) {
623 is_closed = true;
624 break;
625 }
626 }
627 }
628
629 if is_script_block && is_closed {
630 return Err(Pep723Error::DuplicateBlock);
631 }
632 }
633
634 let prelude = prelude.to_string();
636 let metadata = toml.join("\n") + "\n";
637 let postlude = postlude.join("\n") + "\n";
638
639 Ok(Some(Self {
640 prelude,
641 metadata,
642 postlude,
643 }))
644 }
645}
646
647fn extract_shebang(contents: &[u8]) -> Result<(String, String), Pep723Error> {
650 let contents = std::str::from_utf8(contents)?;
651
652 if contents.starts_with("#!") {
653 let bytes = contents.as_bytes();
655 let index = bytes
656 .iter()
657 .position(|&b| b == b'\r' || b == b'\n')
658 .unwrap_or(bytes.len());
659
660 let width = match bytes.get(index) {
662 Some(b'\r') => {
663 if bytes.get(index + 1) == Some(&b'\n') {
664 2
665 } else {
666 1
667 }
668 }
669 Some(b'\n') => 1,
670 _ => 0,
671 };
672
673 let shebang = contents[..index].to_string();
675 let script = contents[index + width..].to_string();
676
677 Ok((shebang, script))
678 } else {
679 Ok((String::new(), contents.to_string()))
680 }
681}
682
683fn serialize_metadata(metadata: &str) -> String {
685 let mut output = String::with_capacity(metadata.len() + 32);
686
687 output.push_str("# /// script");
688 output.push('\n');
689
690 for line in metadata.lines() {
691 output.push('#');
692 if !line.is_empty() {
693 output.push(' ');
694 output.push_str(line);
695 }
696 output.push('\n');
697 }
698
699 output.push_str("# ///");
700 output.push('\n');
701
702 output
703}
704
705#[cfg(test)]
706mod tests {
707 use crate::{Pep723Error, Pep723Script, ScriptTag, serialize_metadata};
708 use std::str::FromStr;
709
710 #[test]
711 fn missing_space() {
712 let contents = indoc::indoc! {r"
713 # /// script
714 #requires-python = '>=3.11'
715 # ///
716 "};
717
718 assert!(matches!(
719 ScriptTag::parse(contents.as_bytes()),
720 Err(Pep723Error::UnclosedBlock)
721 ));
722 }
723
724 #[test]
725 fn no_closing_pragma() {
726 let contents = indoc::indoc! {r"
727 # /// script
728 # requires-python = '>=3.11'
729 # dependencies = [
730 # 'requests<3',
731 # 'rich',
732 # ]
733 "};
734
735 assert!(matches!(
736 ScriptTag::parse(contents.as_bytes()),
737 Err(Pep723Error::UnclosedBlock)
738 ));
739 }
740
741 #[test]
742 fn leading_content() {
743 let contents = indoc::indoc! {r"
744 pass # /// script
745 # requires-python = '>=3.11'
746 # dependencies = [
747 # 'requests<3',
748 # 'rich',
749 # ]
750 # ///
751 #
752 #
753 "};
754
755 assert_eq!(ScriptTag::parse(contents.as_bytes()).unwrap(), None);
756 }
757
758 #[test]
759 fn simple() {
760 let contents = indoc::indoc! {r"
761 # /// script
762 # requires-python = '>=3.11'
763 # dependencies = [
764 # 'requests<3',
765 # 'rich',
766 # ]
767 # ///
768
769 import requests
770 from rich.pretty import pprint
771
772 resp = requests.get('https://peps.python.org/api/peps.json')
773 data = resp.json()
774 "};
775
776 let expected_metadata = indoc::indoc! {r"
777 requires-python = '>=3.11'
778 dependencies = [
779 'requests<3',
780 'rich',
781 ]
782 "};
783
784 let expected_data = indoc::indoc! {r"
785
786 import requests
787 from rich.pretty import pprint
788
789 resp = requests.get('https://peps.python.org/api/peps.json')
790 data = resp.json()
791 "};
792
793 let actual = ScriptTag::parse(contents.as_bytes()).unwrap().unwrap();
794
795 assert_eq!(actual.prelude, String::new());
796 assert_eq!(actual.metadata, expected_metadata);
797 assert_eq!(actual.postlude, expected_data);
798 }
799
800 #[test]
801 fn simple_with_shebang() {
802 let contents = indoc::indoc! {r"
803 #!/usr/bin/env python3
804 # /// script
805 # requires-python = '>=3.11'
806 # dependencies = [
807 # 'requests<3',
808 # 'rich',
809 # ]
810 # ///
811
812 import requests
813 from rich.pretty import pprint
814
815 resp = requests.get('https://peps.python.org/api/peps.json')
816 data = resp.json()
817 "};
818
819 let expected_metadata = indoc::indoc! {r"
820 requires-python = '>=3.11'
821 dependencies = [
822 'requests<3',
823 'rich',
824 ]
825 "};
826
827 let expected_data = indoc::indoc! {r"
828
829 import requests
830 from rich.pretty import pprint
831
832 resp = requests.get('https://peps.python.org/api/peps.json')
833 data = resp.json()
834 "};
835
836 let actual = ScriptTag::parse(contents.as_bytes()).unwrap().unwrap();
837
838 assert_eq!(actual.prelude, "#!/usr/bin/env python3\n".to_string());
839 assert_eq!(actual.metadata, expected_metadata);
840 assert_eq!(actual.postlude, expected_data);
841 }
842
843 #[test]
844 fn embedded_comment() {
845 let contents = indoc::indoc! {r"
846 # /// script
847 # embedded-csharp = '''
848 # /// <summary>
849 # /// text
850 # ///
851 # /// </summary>
852 # public class MyClass { }
853 # '''
854 # ///
855 "};
856
857 let expected = indoc::indoc! {r"
858 embedded-csharp = '''
859 /// <summary>
860 /// text
861 ///
862 /// </summary>
863 public class MyClass { }
864 '''
865 "};
866
867 let actual = ScriptTag::parse(contents.as_bytes())
868 .unwrap()
869 .unwrap()
870 .metadata;
871
872 assert_eq!(actual, expected);
873 }
874
875 #[test]
876 fn trailing_lines() {
877 let contents = indoc::indoc! {r"
878 # /// script
879 # requires-python = '>=3.11'
880 # dependencies = [
881 # 'requests<3',
882 # 'rich',
883 # ]
884 # ///
885 #
886 #
887 "};
888
889 let expected = indoc::indoc! {r"
890 requires-python = '>=3.11'
891 dependencies = [
892 'requests<3',
893 'rich',
894 ]
895 "};
896
897 let actual = ScriptTag::parse(contents.as_bytes())
898 .unwrap()
899 .unwrap()
900 .metadata;
901
902 assert_eq!(actual, expected);
903 }
904
905 #[test]
906 fn unclosed_second_script_block_is_not_duplicate() {
907 let contents = indoc::indoc! {r#"
908 # /// script
909 # dependencies = ["requests"]
910 # ///
911
912 print("Hello, world!")
913
914 # /// script
915 "#};
916
917 assert!(ScriptTag::parse(contents.as_bytes()).is_ok());
918 }
919
920 #[test]
921 fn other_script_block_is_ignored() {
922 let contents = indoc::indoc! {r#"
923 # /// script
924 # dependencies = ["requests"]
925 # ///
926
927
928 # /// other
929 # /// script
930 # ///
931
932 print("Hello, world!")
933 "#};
934
935 assert!(ScriptTag::parse(contents.as_bytes()).is_ok());
936 }
937
938 #[test]
939 fn serialize_metadata_formatting() {
940 let metadata = indoc::indoc! {r"
941 requires-python = '>=3.11'
942 dependencies = [
943 'requests<3',
944 'rich',
945 ]
946 "};
947
948 let expected_output = indoc::indoc! {r"
949 # /// script
950 # requires-python = '>=3.11'
951 # dependencies = [
952 # 'requests<3',
953 # 'rich',
954 # ]
955 # ///
956 "};
957
958 let result = serialize_metadata(metadata);
959 assert_eq!(result, expected_output);
960 }
961
962 #[test]
963 fn serialize_metadata_empty() {
964 let metadata = "";
965 let expected_output = "# /// script\n# ///\n";
966
967 let result = serialize_metadata(metadata);
968 assert_eq!(result, expected_output);
969 }
970
971 #[test]
972 fn script_init_empty() {
973 let contents = "".as_bytes();
974 let (prelude, metadata, postlude) =
975 Pep723Script::init_metadata(contents, &uv_pep440::VersionSpecifiers::default())
976 .unwrap();
977 assert_eq!(prelude, "");
978 assert_eq!(
979 metadata.raw,
980 indoc::indoc! {r"
981 dependencies = []
982 "}
983 );
984 assert_eq!(postlude, "");
985 }
986
987 #[test]
988 fn script_init_requires_python() {
989 let contents = "".as_bytes();
990 let (prelude, metadata, postlude) = Pep723Script::init_metadata(
991 contents,
992 &uv_pep440::VersionSpecifiers::from_str(">=3.8").unwrap(),
993 )
994 .unwrap();
995 assert_eq!(prelude, "");
996 assert_eq!(
997 metadata.raw,
998 indoc::indoc! {r#"
999 requires-python = ">=3.8"
1000 dependencies = []
1001 "#}
1002 );
1003 assert_eq!(postlude, "");
1004 }
1005
1006 #[test]
1007 fn script_init_with_hashbang() {
1008 let contents = indoc::indoc! {r#"
1009 #!/usr/bin/env python3
1010
1011 print("Hello, world!")
1012 "#}
1013 .as_bytes();
1014 let (prelude, metadata, postlude) =
1015 Pep723Script::init_metadata(contents, &uv_pep440::VersionSpecifiers::default())
1016 .unwrap();
1017 assert_eq!(prelude, "#!/usr/bin/env python3\n");
1018 assert_eq!(
1019 metadata.raw,
1020 indoc::indoc! {r"
1021 dependencies = []
1022 "}
1023 );
1024 assert_eq!(
1025 postlude,
1026 indoc::indoc! {r#"
1027
1028 print("Hello, world!")
1029 "#}
1030 );
1031 }
1032
1033 #[test]
1034 fn script_init_with_other_metadata() {
1035 let contents = indoc::indoc! {r#"
1036 # /// noscript
1037 # Hello,
1038 #
1039 # World!
1040 # ///
1041
1042 print("Hello, world!")
1043 "#}
1044 .as_bytes();
1045 let (prelude, metadata, postlude) =
1046 Pep723Script::init_metadata(contents, &uv_pep440::VersionSpecifiers::default())
1047 .unwrap();
1048 assert_eq!(prelude, "");
1049 assert_eq!(
1050 metadata.raw,
1051 indoc::indoc! {r"
1052 dependencies = []
1053 "}
1054 );
1055 assert_eq!(
1057 postlude,
1058 indoc::indoc! {r#"
1059
1060 # /// noscript
1061 # Hello,
1062 #
1063 # World!
1064 # ///
1065
1066 print("Hello, world!")
1067 "#}
1068 );
1069 }
1070
1071 #[test]
1072 fn script_init_with_hashbang_and_other_metadata() {
1073 let contents = indoc::indoc! {r#"
1074 #!/usr/bin/env python3
1075 # /// noscript
1076 # Hello,
1077 #
1078 # World!
1079 # ///
1080
1081 print("Hello, world!")
1082 "#}
1083 .as_bytes();
1084 let (prelude, metadata, postlude) =
1085 Pep723Script::init_metadata(contents, &uv_pep440::VersionSpecifiers::default())
1086 .unwrap();
1087 assert_eq!(prelude, "#!/usr/bin/env python3\n");
1088 assert_eq!(
1089 metadata.raw,
1090 indoc::indoc! {r"
1091 dependencies = []
1092 "}
1093 );
1094 assert_eq!(
1096 postlude,
1097 indoc::indoc! {r#"
1098
1099 # /// noscript
1100 # Hello,
1101 #
1102 # World!
1103 # ///
1104
1105 print("Hello, world!")
1106 "#}
1107 );
1108 }
1109
1110 #[test]
1111 fn script_init_with_valid_metadata_line() {
1112 let contents = indoc::indoc! {r#"
1113 # Hello,
1114 # /// noscript
1115 #
1116 # World!
1117 # ///
1118
1119 print("Hello, world!")
1120 "#}
1121 .as_bytes();
1122 let (prelude, metadata, postlude) =
1123 Pep723Script::init_metadata(contents, &uv_pep440::VersionSpecifiers::default())
1124 .unwrap();
1125 assert_eq!(prelude, "");
1126 assert_eq!(
1127 metadata.raw,
1128 indoc::indoc! {r"
1129 dependencies = []
1130 "}
1131 );
1132 assert_eq!(
1134 postlude,
1135 indoc::indoc! {r#"
1136
1137 # Hello,
1138 # /// noscript
1139 #
1140 # World!
1141 # ///
1142
1143 print("Hello, world!")
1144 "#}
1145 );
1146 }
1147
1148 #[test]
1149 fn script_init_with_valid_empty_metadata_line() {
1150 let contents = indoc::indoc! {r#"
1151 #
1152 # /// noscript
1153 # Hello,
1154 # World!
1155 # ///
1156
1157 print("Hello, world!")
1158 "#}
1159 .as_bytes();
1160 let (prelude, metadata, postlude) =
1161 Pep723Script::init_metadata(contents, &uv_pep440::VersionSpecifiers::default())
1162 .unwrap();
1163 assert_eq!(prelude, "");
1164 assert_eq!(
1165 metadata.raw,
1166 indoc::indoc! {r"
1167 dependencies = []
1168 "}
1169 );
1170 assert_eq!(
1172 postlude,
1173 indoc::indoc! {r#"
1174
1175 #
1176 # /// noscript
1177 # Hello,
1178 # World!
1179 # ///
1180
1181 print("Hello, world!")
1182 "#}
1183 );
1184 }
1185
1186 #[test]
1187 fn script_init_with_non_metadata_comment() {
1188 let contents = indoc::indoc! {r#"
1189 #Hello,
1190 # /// noscript
1191 #
1192 # World!
1193 # ///
1194
1195 print("Hello, world!")
1196 "#}
1197 .as_bytes();
1198 let (prelude, metadata, postlude) =
1199 Pep723Script::init_metadata(contents, &uv_pep440::VersionSpecifiers::default())
1200 .unwrap();
1201 assert_eq!(prelude, "");
1202 assert_eq!(
1203 metadata.raw,
1204 indoc::indoc! {r"
1205 dependencies = []
1206 "}
1207 );
1208 assert_eq!(
1209 postlude,
1210 indoc::indoc! {r#"
1211 #Hello,
1212 # /// noscript
1213 #
1214 # World!
1215 # ///
1216
1217 print("Hello, world!")
1218 "#}
1219 );
1220 }
1221}