Skip to main content

package_parser/pkgs/python/
pysetup_cfg.rs

1use anyhow::bail;
2use anyhow::Result as AnyhowResult;
3use lazy_static::lazy_static;
4use regex::Regex;
5
6use crate::error::SourcePkgError;
7use crate::pkgs::common::model::{Package, PackageManifest};
8use crate::pkgs::pyrequirements::PyRequirements;
9
10use std::fs::File;
11use std::io::Read;
12use std::path::Path;
13
14lazy_static! {
15    static ref KEY_VALUE_REGEX: Regex = Regex::new("(?x)(?P<key>(.*))=(?P<value>(.*))").unwrap();
16}
17
18fn strip_comment(line: &str) -> &str {
19    line.find(';').map(|i| &line[..i]).unwrap_or(line).trim()
20}
21
22fn parse_python_requirement(content: &str, key_name: &str) -> AnyhowResult<Vec<String>> {
23    //configparser does not do multi line values
24    //ini dies on them as well.
25    //so we do our own poor man's parsing
26    //debug!("Parsing {:?}", &setup_cfg_file);
27    let raw = content;
28    let mut res = Vec::new();
29    match raw.find("[options]") {
30        Some(options_start) => {
31            let mut inside_value = false;
32            let mut value_indention = 0;
33            let mut value = "".to_string();
34            for line in raw[options_start..].split('\n') {
35                if !inside_value {
36                    if line.contains(key_name) {
37                        let wo_indent_len = (line.replace('\t', "    ").trim_start()).len();
38                        value_indention = line.len() - wo_indent_len;
39                        match line.find('=') {
40                            Some(equal_pos) => {
41                                let v = line[equal_pos + 1..].trim_end();
42                                value += v;
43                                value += "\n";
44                                inside_value = true;
45                            }
46                            None => bail!("No = in install_requires line"),
47                        }
48                    }
49                } else {
50                    // inside value
51                    let wo_indent_len = (line.replace('\t', "    ").trim_start()).len();
52                    let indent = line.len() - wo_indent_len;
53                    if indent > value_indention {
54                        value += line.trim_start();
55                        value += "\n"
56                    } else {
57                        break;
58                    }
59                }
60            }
61            for line in value.split('\n') {
62                if !line.trim().is_empty() {
63                    let line = strip_comment(line);
64                    res.push(line.trim().to_string())
65                }
66            }
67        }
68        None => bail!("no [options] in setup.cfg"),
69    };
70    Ok(res)
71}
72
73fn parse_python_metadata(content: &str) -> AnyhowResult<Package> {
74    let mut enter_metadata = false;
75    let mut package = Package::default();
76    for line in content.lines() {
77        let line = strip_comment(line);
78        if line.contains("[metadata]") {
79            enter_metadata = true;
80            continue;
81        }
82        if line.contains('[') {
83            enter_metadata = false;
84            continue;
85        }
86
87        if enter_metadata {
88            let captures = match KEY_VALUE_REGEX.captures(line) {
89                Some(captures) => captures,
90                None => continue,
91            };
92            let key = captures
93                .name("key")
94                .map(|v| v.as_str())
95                .unwrap_or("")
96                .trim();
97            let value = captures
98                .name("value")
99                .map(|v| v.as_str())
100                .unwrap_or("")
101                .trim();
102
103            if key == "name" {
104                package.name = value.to_string();
105            } else if key == "version" {
106                package.version = value.to_string();
107            }
108        }
109    }
110
111    Ok(package)
112}
113
114pub struct PySetupCfg {}
115
116impl PySetupCfg {
117    pub fn new() -> Self {
118        Self {}
119    }
120
121    fn parse(path: impl AsRef<Path>) -> Result<Package, SourcePkgError> {
122        let mut file = File::open(path)?;
123        let mut content = String::new();
124        file.read_to_string(&mut content)?;
125
126        let mut package = match parse_python_metadata(&content.clone()) {
127            Ok(package) => package,
128            Err(e) => {
129                log::warn!("Failed to parse python metadata: {}", e);
130                Package::default()
131            }
132        };
133
134        let mut install_requires =
135            match parse_python_requirement(&content.clone(), "install_requires") {
136                Ok(install_requires) => {
137                    PyRequirements::parse_requirement_content(&install_requires.join("\n"))
138                        .unwrap_or_default()
139                }
140                Err(_) => vec![],
141            };
142
143        let mut setup_requires = match parse_python_requirement(&content.clone(), "setup_requires")
144        {
145            Ok(setup_requires) => {
146                PyRequirements::parse_requirement_content(&setup_requires.join("\n"))
147                    .unwrap_or_default()
148            }
149            Err(_) => vec![],
150        };
151
152        let mut test_requires = match parse_python_requirement(&content.clone(), "test_requires") {
153            Ok(test_requires) => {
154                PyRequirements::parse_requirement_content(&test_requires.join("\n"))
155                    .unwrap_or_default()
156            }
157            Err(_) => vec![],
158        };
159
160        install_requires.iter_mut().for_each(|r| {
161            r.is_runtime = true;
162            r.is_optional = false;
163            r.is_resolved = false;
164            r.scope = "install_require".into();
165        });
166
167        setup_requires.iter_mut().for_each(|r| {
168            r.is_runtime = false;
169            r.is_optional = true;
170            r.is_resolved = false;
171            r.scope = "setup_require".into();
172        });
173
174        test_requires.iter_mut().for_each(|r| {
175            r.is_runtime = false;
176            r.is_optional = true;
177            r.is_resolved = false;
178            r.scope = "test_require".into();
179        });
180
181        install_requires.append(&mut setup_requires);
182        install_requires.append(&mut test_requires);
183        package.dependencies = install_requires;
184
185        Ok(package)
186    }
187}
188
189#[async_trait::async_trait]
190impl PackageManifest for PySetupCfg {
191    fn get_name(&self) -> String {
192        "pypi".to_string()
193    }
194
195    async fn recognize(&self, path: &Path) -> Result<Package, SourcePkgError> {
196        Self::parse(path)
197    }
198
199    fn file_name_patterns(&self) -> &'static [&'static str] {
200        &["setup.cfg", "*setup.cfg"]
201    }
202}
203
204#[cfg(test)]
205mod tests {
206    use super::*;
207
208    #[test]
209    fn test_parse_setup_cfg() {
210        let filepath = Path::new(concat!(
211            env!("CARGO_MANIFEST_DIR"),
212            "/testdata/pypi/unpacked_sdist/metadata-2.1/commoncode-21.5.12/setup.cfg"
213        ));
214
215        let p = PySetupCfg::parse(filepath).unwrap();
216        println!("{:?}", p);
217    }
218}