Skip to main content

package_parser/pkgs/python/
pyrequirements.rs

1use 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}