package_parser/pkgs/python/
pyproject.rs1use std::{collections::HashMap, path::Path};
4
5use crate::types::Package;
6use serde::Deserialize;
7
8use crate::{error::SourcePkgError, pkgs::python::poetrylock, PackageManifest};
9
10lazy_static::lazy_static! {
11 static ref NORMALIZE_NAME: regex::Regex = regex::Regex::new(r"[-_.]+").unwrap();
12}
13
14#[derive(Debug, Deserialize, Default)]
15#[serde(default)]
16pub struct ProjectSpec {
17 pub tool: ProjectSpecTool,
18}
19
20#[derive(Debug, Deserialize, Default)]
21#[serde(default)]
22pub struct ProjectSpecTool {
23 pub poetry: Option<PoetryTool>,
24}
25
26#[derive(Debug, Deserialize, Default)]
27#[serde(default, rename_all = "kebab-case")]
28pub struct PoetryTool {
29 dependencies: HashMap<String, PoetryDependency>,
30 dev_dependencies: HashMap<String, PoetryDependency>,
32 group: HashMap<String, PoetryGroup>,
33}
34
35impl PoetryTool {
36 pub fn has_dependency(&self, name: &str) -> bool {
37 if self.dependencies.contains_key(name) {
38 return true;
39 }
40
41 if self.dev_dependencies.contains_key(name) {
42 return true;
43 }
44
45 for group in self.group.values() {
46 if group.dependencies.contains_key(name) {
47 return true;
48 }
49 }
50
51 false
52 }
53}
54
55#[derive(Debug, Deserialize, Default)]
56#[serde(default)]
57struct PoetryGroup {
58 dependencies: HashMap<String, PoetryDependency>,
59}
60
61#[derive(Debug, Deserialize)]
62#[serde(untagged)]
63#[allow(unused)]
64enum PoetryDependency {
65 Compact(String),
66 Expanded(PoetryExpandedDependency),
67 List(Vec<PoetryExpandedDependency>),
68}
69
70#[allow(dead_code)]
71#[derive(Debug, Deserialize)]
72struct PoetryExpandedDependency {
73 version: Option<String>,
74 markers: Option<String>,
75}
76
77pub struct PyProject {}
78
79impl PyProject {
80 pub fn new() -> Self {
81 Self {}
82 }
83
84 fn parse(path: &Path) -> Result<Package, SourcePkgError> {
85 let path_dir = path.parent().unwrap();
86
87 let manifest_content = std::fs::read_to_string(path)?;
88 let manifest: ProjectSpec = toml::from_str(&manifest_content)?;
89
90 if let Some(mut poetry) = manifest.tool.poetry {
91 let normalize_name =
93 |name: &str| -> String { NORMALIZE_NAME.replace_all(name, "-").to_lowercase() };
94
95 let deps = poetry
96 .dependencies
97 .drain()
98 .map(|(name, dep)| (normalize_name(&name), dep))
99 .collect();
100 poetry.dependencies = deps;
101
102 let dev_deps = poetry
103 .dev_dependencies
104 .drain()
105 .map(|(name, dep)| (normalize_name(&name), dep))
106 .collect();
107 poetry.dev_dependencies = dev_deps;
108
109 for group in poetry.group.values_mut() {
110 let deps = group
111 .dependencies
112 .drain()
113 .map(|(name, dep)| (normalize_name(&name), dep))
114 .collect();
115 group.dependencies = deps;
116 }
117
118 let lock_path = path_dir.join("poetry.lock");
120 if lock_path.exists() {
121 return poetrylock::process(&lock_path, &poetry);
122 }
123 }
124
125 Ok(Default::default())
126 }
127}
128
129#[async_trait::async_trait]
130impl PackageManifest for PyProject {
131 fn get_name(&self) -> String {
132 "pypi".to_string()
133 }
134
135 async fn recognize(&self, path: &Path) -> Result<Package, SourcePkgError> {
136 Self::parse(path)
137 }
138
139 fn file_name_patterns(&self) -> &'static [&'static str] {
140 &["pyproject.toml"]
141 }
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147
148 #[test]
149 fn test_poetry_with_lock() {
150 let filepath = Path::new(concat!(
151 env!("CARGO_MANIFEST_DIR"),
152 "/testdata/pypi/poetry/pyproject.toml"
153 ));
154
155 let p = PyProject::parse(filepath).unwrap();
156 println!("{:?}", p);
157 }
158}