changepacks_python/
finder.rs

1use anyhow::{Context, Result};
2use async_trait::async_trait;
3use changepacks_core::{Project, ProjectFinder};
4use std::{
5    collections::HashMap,
6    path::{Path, PathBuf},
7};
8use tokio::fs::read_to_string;
9
10use crate::{package::PythonPackage, workspace::PythonWorkspace};
11
12#[derive(Debug)]
13pub struct PythonProjectFinder {
14    projects: HashMap<PathBuf, Project>,
15    project_files: Vec<&'static str>,
16}
17
18impl Default for PythonProjectFinder {
19    fn default() -> Self {
20        Self::new()
21    }
22}
23
24impl PythonProjectFinder {
25    pub fn new() -> Self {
26        Self {
27            projects: HashMap::new(),
28            project_files: vec!["pyproject.toml"],
29        }
30    }
31}
32
33#[async_trait]
34impl ProjectFinder for PythonProjectFinder {
35    fn projects(&self) -> Vec<&Project> {
36        self.projects.values().collect::<Vec<_>>()
37    }
38    fn projects_mut(&mut self) -> Vec<&mut Project> {
39        self.projects.values_mut().collect::<Vec<_>>()
40    }
41
42    fn project_files(&self) -> &[&str] {
43        &self.project_files
44    }
45
46    async fn visit(&mut self, path: &Path, relative_path: &Path) -> Result<()> {
47        if path.is_file()
48            && self.project_files().contains(
49                &path
50                    .file_name()
51                    .context(format!("File name not found - {}", path.display()))?
52                    .to_str()
53                    .context(format!("File name not found - {}", path.display()))?,
54            )
55        {
56            if self.projects.contains_key(path) {
57                return Ok(());
58            }
59            // read pyproject.toml
60            let pyproject_toml = read_to_string(path).await?;
61            let pyproject_toml: toml::Value = toml::from_str(&pyproject_toml)?;
62            let project = pyproject_toml
63                .get("project")
64                .context(format!("Project not found - {}", path.display()))?;
65
66            // if workspace
67            if pyproject_toml
68                .get("tool")
69                .and_then(|t| t.get("uv").and_then(|u| u.get("workspace")))
70                .is_some()
71            {
72                let version = project["version"].as_str().map(|v| v.to_string());
73                let name = project["name"].as_str().map(|v| v.to_string());
74                self.projects.insert(
75                    path.to_path_buf(),
76                    Project::Workspace(Box::new(PythonWorkspace::new(
77                        name,
78                        version,
79                        path.to_path_buf(),
80                        relative_path.to_path_buf(),
81                    ))),
82                );
83            } else {
84                let version = project
85                    .get("version")
86                    .and_then(|v| v.as_str())
87                    .map(|v| v.to_string())
88                    .context(format!("Version not found - {}", path.display()))?;
89                let name = project
90                    .get("name")
91                    .and_then(|v| v.as_str())
92                    .map(|v| v.to_string())
93                    .context(format!("Name not found - {}", path.display()))?;
94                self.projects.insert(
95                    path.to_path_buf(),
96                    Project::Package(Box::new(PythonPackage::new(
97                        name,
98                        version,
99                        path.to_path_buf(),
100                        relative_path.to_path_buf(),
101                    ))),
102                );
103            }
104        }
105        Ok(())
106    }
107}