1use crate::ManifestParser;
4use kdo_core::{DepKind, Dependency, KdoError, Language, Project};
5use std::path::Path;
6use tracing::debug;
7
8pub struct PythonParser;
10
11impl ManifestParser for PythonParser {
12 fn manifest_name(&self) -> &str {
13 "pyproject.toml"
14 }
15
16 fn can_parse(&self, manifest_path: &Path) -> bool {
17 manifest_path
18 .file_name()
19 .map(|f| f == "pyproject.toml")
20 .unwrap_or(false)
21 }
22
23 fn parse(
24 &self,
25 manifest_path: &Path,
26 _workspace_root: &Path,
27 ) -> Result<(Project, Vec<Dependency>), KdoError> {
28 let content = std::fs::read_to_string(manifest_path)?;
29 let doc: toml::Value = toml::from_str(&content).map_err(|e| KdoError::ParseError {
30 path: manifest_path.to_path_buf(),
31 source: e.into(),
32 })?;
33
34 let (name, description) = if let Some(project) = doc.get("project") {
36 let name = project
37 .get("name")
38 .and_then(|v| v.as_str())
39 .unwrap_or("unknown")
40 .to_string();
41 let desc = project
42 .get("description")
43 .and_then(|v| v.as_str())
44 .map(String::from);
45 (name, desc)
46 } else if let Some(poetry) = doc.get("tool").and_then(|t| t.get("poetry")) {
47 let name = poetry
48 .get("name")
49 .and_then(|v| v.as_str())
50 .unwrap_or("unknown")
51 .to_string();
52 let desc = poetry
53 .get("description")
54 .and_then(|v| v.as_str())
55 .map(String::from);
56 (name, desc)
57 } else {
58 return Err(KdoError::ParseError {
59 path: manifest_path.to_path_buf(),
60 source: anyhow::anyhow!("no [project] or [tool.poetry] table found"),
61 });
62 };
63
64 let project_dir = manifest_path
65 .parent()
66 .unwrap_or(Path::new("."))
67 .to_path_buf();
68
69 debug!(name = %name, "parsed pyproject.toml");
70
71 let mut deps = Vec::new();
72
73 if let Some(dep_list) = doc
75 .get("project")
76 .and_then(|p| p.get("dependencies"))
77 .and_then(|d| d.as_array())
78 {
79 for dep_val in dep_list {
80 if let Some(dep_str) = dep_val.as_str() {
81 let (dep_name, version_req) = parse_pep508(dep_str);
82 deps.push(Dependency {
83 name: dep_name,
84 version_req,
85 kind: DepKind::Source,
86 is_workspace: false,
87 resolved_path: None,
88 });
89 }
90 }
91 }
92
93 if let Some(dev_list) = doc
95 .get("project")
96 .and_then(|p| p.get("optional-dependencies"))
97 .and_then(|o| o.get("dev"))
98 .and_then(|d| d.as_array())
99 {
100 for dep_val in dev_list {
101 if let Some(dep_str) = dep_val.as_str() {
102 let (dep_name, version_req) = parse_pep508(dep_str);
103 deps.push(Dependency {
104 name: dep_name,
105 version_req,
106 kind: DepKind::Dev,
107 is_workspace: false,
108 resolved_path: None,
109 });
110 }
111 }
112 }
113
114 let project = Project {
115 name,
116 path: project_dir,
117 language: Language::Python,
118 manifest_path: manifest_path.to_path_buf(),
119 context_summary: description,
120 public_api_files: Vec::new(),
121 internal_files: Vec::new(),
122 content_hash: [0u8; 32],
123 };
124
125 Ok((project, deps))
126 }
127}
128
129fn parse_pep508(spec: &str) -> (String, String) {
131 let spec = spec.trim();
132 let split_pos = spec
134 .find(['>', '<', '=', '!', '~', '[', ';'])
135 .unwrap_or(spec.len());
136 let name = spec[..split_pos].trim().to_string();
137 let version = spec[split_pos..].trim().to_string();
138 (name, version)
139}