normalize_manifest/
pipfile.rs1use crate::{DeclaredDep, DepKind, ManifestError, ManifestParser, ParsedManifest};
4use toml::Value;
5
6pub struct PipfileParser;
8
9impl ManifestParser for PipfileParser {
10 fn filename(&self) -> &'static str {
11 "Pipfile"
12 }
13
14 fn parse(&self, content: &str) -> Result<ParsedManifest, ManifestError> {
15 let toml: Value = content
16 .parse::<Value>()
17 .map_err(|e| ManifestError(e.to_string()))?;
18
19 let mut deps = Vec::new();
20
21 parse_section(&toml, "packages", DepKind::Normal, &mut deps);
22 parse_section(&toml, "dev-packages", DepKind::Dev, &mut deps);
23
24 Ok(ParsedManifest {
25 ecosystem: "pip",
26 name: None,
27 version: None,
28 dependencies: deps,
29 })
30 }
31}
32
33fn parse_section(toml: &Value, section: &str, kind: DepKind, deps: &mut Vec<DeclaredDep>) {
35 let Some(table) = toml.get(section).and_then(|v| v.as_table()) else {
36 return;
37 };
38
39 for (name, val) in table {
40 let version_req = dep_version_req(val);
41 deps.push(DeclaredDep {
42 name: name.clone(),
43 version_req,
44 kind,
45 });
46 }
47}
48
49fn dep_version_req(val: &Value) -> Option<String> {
56 match val {
57 Value::String(s) => version_string(s),
58 Value::Table(t) => t
59 .get("version")
60 .and_then(|v| v.as_str())
61 .and_then(version_string),
62 _ => None,
63 }
64}
65
66fn version_string(s: &str) -> Option<String> {
68 if s == "*" { None } else { Some(s.to_string()) }
69}
70
71#[cfg(test)]
72mod tests {
73 use super::*;
74 use crate::ManifestParser;
75
76 const SAMPLE: &str = r#"
77[[source]]
78url = "https://pypi.org/simple"
79verify_ssl = true
80name = "pypi"
81
82[packages]
83requests = "*"
84flask = ">=2.0"
85click = {version = ">=8.0", extras = ["dev"]}
86django = {version = "*", markers = "python_version >= '3.8'"}
87sqlalchemy = {git = "https://github.com/sqlalchemy/sqlalchemy.git", ref = "main"}
88
89[dev-packages]
90pytest = ">=7.0"
91black = "*"
92mypy = {version = ">=1.0"}
93
94[requires]
95python_version = "3.11"
96"#;
97
98 #[test]
99 fn test_ecosystem_and_no_name() {
100 let m = PipfileParser.parse(SAMPLE).unwrap();
101 assert_eq!(m.ecosystem, "pip");
102 assert!(m.name.is_none());
103 assert!(m.version.is_none());
104 }
105
106 #[test]
107 fn test_normal_deps() {
108 let m = PipfileParser.parse(SAMPLE).unwrap();
109 let normal: Vec<_> = m
110 .dependencies
111 .iter()
112 .filter(|d| d.kind == DepKind::Normal)
113 .collect();
114 assert_eq!(normal.len(), 5);
115
116 let requests = normal.iter().find(|d| d.name == "requests").unwrap();
118 assert!(requests.version_req.is_none());
119
120 let flask = normal.iter().find(|d| d.name == "flask").unwrap();
122 assert_eq!(flask.version_req.as_deref(), Some(">=2.0"));
123
124 let click = normal.iter().find(|d| d.name == "click").unwrap();
126 assert_eq!(click.version_req.as_deref(), Some(">=8.0"));
127
128 let django = normal.iter().find(|d| d.name == "django").unwrap();
130 assert!(django.version_req.is_none());
131
132 let sqlalchemy = normal.iter().find(|d| d.name == "sqlalchemy").unwrap();
134 assert!(sqlalchemy.version_req.is_none());
135 }
136
137 #[test]
138 fn test_dev_deps() {
139 let m = PipfileParser.parse(SAMPLE).unwrap();
140 let dev: Vec<_> = m
141 .dependencies
142 .iter()
143 .filter(|d| d.kind == DepKind::Dev)
144 .collect();
145 assert_eq!(dev.len(), 3);
146
147 let pytest = dev.iter().find(|d| d.name == "pytest").unwrap();
148 assert_eq!(pytest.version_req.as_deref(), Some(">=7.0"));
149
150 let black = dev.iter().find(|d| d.name == "black").unwrap();
151 assert!(black.version_req.is_none());
152
153 let mypy = dev.iter().find(|d| d.name == "mypy").unwrap();
154 assert_eq!(mypy.version_req.as_deref(), Some(">=1.0"));
155 }
156
157 #[test]
158 fn test_empty_pipfile() {
159 let m = PipfileParser.parse("").unwrap();
160 assert!(m.dependencies.is_empty());
161 }
162
163 #[test]
164 fn test_no_dev_section() {
165 let content = r#"
166[packages]
167requests = ">=2.28"
168"#;
169 let m = PipfileParser.parse(content).unwrap();
170 assert_eq!(m.dependencies.len(), 1);
171 assert_eq!(m.dependencies[0].kind, DepKind::Normal);
172 assert_eq!(m.dependencies[0].version_req.as_deref(), Some(">=2.28"));
173 }
174}