package_parser/pkgs/python/
pyrequirements.rs1use packageurl::PackageUrl;
2use requirements::enums::Comparison;
3use crate::types::DependentPackage;
4
5use crate::error::SourcePkgError;
6use crate::pkgs::common::model::{Package, PackageManifest};
7
8use std::path::Path;
9
10pub struct PyRequirements {}
11
12impl PyRequirements {
13 pub fn new() -> Self {
14 Self {}
15 }
16
17 fn specs_to_string(specs: &[(Comparison, String)]) -> String {
18 specs
19 .iter()
20 .map(|(comparison, version)| format!("{}{}", comparison, version))
21 .collect::<Vec<_>>()
22 .join(",")
23 }
24
25 pub fn parse_requirement_content(
26 content: &str,
27 ) -> Result<Vec<DependentPackage>, SourcePkgError> {
28 let requirements =
29 requirements::parse_str(content).map_err(SourcePkgError::GenericsError2)?;
30
31 let mut dependencies = vec![];
32
33 for requirement in requirements {
34 let name = if let Some(name) = requirement.name {
35 super::normalize_name(&name)
36 } else {
37 continue;
38 };
39
40 let mut exact_version = None;
41
42 if requirement.specs.len() == 1 {
43 let (op, version) = &requirement.specs[0];
44
45 if *op == Comparison::Equal || *op == Comparison::ArbitraryEqual {
46 exact_version = Some(version.clone());
47 }
48 }
49
50 let mut purl = PackageUrl::new("pypi", name).unwrap();
51 if let Some(v) = &exact_version {
52 purl.with_version(v);
53 }
54
55 let dependency = DependentPackage {
56 purl: purl.to_string(),
57 is_resolved: exact_version.is_some(),
58 requirement: if let Some(v) = exact_version {
59 v
60 } else {
61 Self::specs_to_string(&requirement.specs)
62 },
63 ..Default::default()
64 };
65
66 dependencies.push(dependency);
67 }
68
69 Ok(dependencies)
70 }
71
72 pub fn parse_requirement(
73 path: impl AsRef<Path>,
74 ) -> Result<Vec<DependentPackage>, SourcePkgError> {
75 let content_bytes = std::fs::read(path)?;
76
77 let mut content = crate::pkgs::common::decode_string(&content_bytes)?;
78
79 if !content.ends_with('\n') {
80 content.push('\n');
81 }
82
83 Self::parse_requirement_content(&content)
84 }
85}
86
87#[async_trait::async_trait]
88impl PackageManifest for PyRequirements {
89 fn get_name(&self) -> String {
90 "pypi".to_string()
91 }
92
93 async fn recognize(&self, path: &Path) -> Result<Package, SourcePkgError> {
94 let dependencies = Self::parse_requirement(path)?;
95 let package = Package {
96 dependencies,
97 ..Default::default()
98 };
99
100 Ok(package)
101 }
102
103 fn file_name_patterns(&self) -> &'static [&'static str] {
104 &[
105 "*requirement*.txt",
106 "*requirement*.pip",
107 "*requirement*.in",
108 "*requires.txt",
109 "*requirements/*.txt",
110 "*requirements/*.pip",
111 "*requirements/*.in",
112 "*reqs.txt",
113 ]
114 }
115}
116
117#[cfg(test)]
118mod test {
119 use super::*;
120 use crate::helper::testing::*;
121 use std::path::Path;
122
123 #[test]
124 fn all_requirements_txt() {
125 let dir_path = Path::new(concat!(
126 env!("CARGO_MANIFEST_DIR"),
127 "/testdata/pypi/requirements_txt"
128 ));
129
130 for result in all_files_with_extensions(dir_path, &["txt"]) {
131 let path = result.expect("Failed to walk");
132 println!("{}", path.display());
133 let _ = PyRequirements::parse_requirement(path).expect("Failed to parse");
134 }
135 }
136}