normalize_manifest/
setup_cfg.rs1use crate::pip::parse_pip_requirement;
9use crate::{DeclaredDep, DepKind, ManifestError, ManifestParser, ParsedManifest};
10
11pub struct SetupCfgParser;
13
14impl ManifestParser for SetupCfgParser {
15 fn filename(&self) -> &'static str {
16 "setup.cfg"
17 }
18
19 fn parse(&self, content: &str) -> Result<ParsedManifest, ManifestError> {
20 let mut name = None;
21 let mut version = None;
22 let mut deps: Vec<DeclaredDep> = Vec::new();
23
24 #[derive(PartialEq)]
25 enum Section {
26 Other,
27 Metadata,
28 Options,
29 Extras,
30 }
31
32 let mut section = Section::Other;
33 let mut collecting_requires = false;
34 let mut current_extras_kind = DepKind::Optional;
35
36 for line in content.lines() {
37 let trimmed = line.trim();
38
39 if trimmed.is_empty() {
41 collecting_requires = false;
42 continue;
43 }
44 if trimmed.starts_with('#') || trimmed.starts_with(';') {
45 continue;
46 }
47
48 if trimmed.starts_with('[') && trimmed.ends_with(']') {
50 collecting_requires = false;
51 let sec = &trimmed[1..trimmed.len() - 1];
52 section = match sec {
53 "metadata" => Section::Metadata,
54 "options" => Section::Options,
55 s if s.starts_with("options.extras_require") => {
56 current_extras_kind = DepKind::Optional;
57 Section::Extras
58 }
59 _ => Section::Other,
60 };
61 continue;
62 }
63
64 if line.starts_with([' ', '\t']) && collecting_requires {
66 let kind = if section == Section::Extras {
67 current_extras_kind
68 } else {
69 DepKind::Normal
70 };
71 if let Some(dep) = parse_pip_requirement(trimmed) {
72 deps.push(DeclaredDep { kind, ..dep });
73 }
74 continue;
75 }
76
77 if let Some(eq_idx) = trimmed.find('=') {
79 collecting_requires = false;
80 let key = trimmed[..eq_idx].trim();
81 let value = trimmed[eq_idx + 1..].trim();
82
83 match section {
84 Section::Metadata => match key {
85 "name" => name = Some(value.to_string()),
86 "version" => version = Some(value.to_string()),
87 _ => {}
88 },
89 Section::Options => {
90 if key == "install_requires" {
91 collecting_requires = true;
92 if !value.is_empty()
94 && let Some(dep) = parse_pip_requirement(value)
95 {
96 deps.push(dep);
97 }
98 }
99 }
100 Section::Extras => {
101 collecting_requires = true;
104 if !value.is_empty()
105 && let Some(dep) = parse_pip_requirement(value)
106 {
107 deps.push(DeclaredDep {
108 kind: DepKind::Optional,
109 ..dep
110 });
111 }
112 }
113 Section::Other => {}
114 }
115 }
116 }
117
118 Ok(ParsedManifest {
119 ecosystem: "python",
120 name,
121 version,
122 dependencies: deps,
123 })
124 }
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130 use crate::ManifestParser;
131
132 #[test]
133 fn test_parse_setup_cfg() {
134 let content = r#"[metadata]
135name = my-package
136version = 1.2.3
137
138[options]
139install_requires =
140 requests>=2.28
141 flask>=2.0
142 numpy
143
144[options.extras_require]
145dev =
146 pytest
147 black
148"#;
149 let m = SetupCfgParser.parse(content).unwrap();
150 assert_eq!(m.ecosystem, "python");
151 assert_eq!(m.name.as_deref(), Some("my-package"));
152 assert_eq!(m.version.as_deref(), Some("1.2.3"));
153
154 let normal: Vec<_> = m
155 .dependencies
156 .iter()
157 .filter(|d| d.kind == DepKind::Normal)
158 .collect();
159 assert_eq!(normal.len(), 3);
160 assert!(normal.iter().any(|d| d.name == "requests"));
161 assert!(normal.iter().any(|d| d.name == "flask"));
162 assert!(normal.iter().any(|d| d.name == "numpy"));
163
164 let optional: Vec<_> = m
165 .dependencies
166 .iter()
167 .filter(|d| d.kind == DepKind::Optional)
168 .collect();
169 assert_eq!(optional.len(), 2);
170 }
171}