package_parser/pkgs/python/
pysetup_cfg.rs1use 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 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 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}