changepacks_python/
workspace.rs

1use anyhow::Result;
2use async_trait::async_trait;
3use changepacks_core::{Language, UpdateType, Workspace};
4use changepacks_utils::next_version;
5use std::path::{Path, PathBuf};
6use tokio::fs::{read_to_string, write};
7use toml_edit::DocumentMut;
8
9#[derive(Debug)]
10pub struct PythonWorkspace {
11    path: PathBuf,
12    relative_path: PathBuf,
13    version: Option<String>,
14    name: Option<String>,
15    is_changed: bool,
16}
17
18impl PythonWorkspace {
19    pub fn new(
20        name: Option<String>,
21        version: Option<String>,
22        path: PathBuf,
23        relative_path: PathBuf,
24    ) -> Self {
25        Self {
26            path,
27            relative_path,
28            name,
29            version,
30            is_changed: false,
31        }
32    }
33}
34
35#[async_trait]
36impl Workspace for PythonWorkspace {
37    fn name(&self) -> Option<&str> {
38        self.name.as_deref()
39    }
40
41    fn path(&self) -> &Path {
42        &self.path
43    }
44
45    fn version(&self) -> Option<&str> {
46        self.version.as_deref()
47    }
48
49    async fn update_version(&mut self, update_type: UpdateType) -> Result<()> {
50        let next_version = next_version(
51            self.version.as_ref().unwrap_or(&String::from("0.0.0")),
52            update_type,
53        )?;
54
55        let pyproject_toml_raw = read_to_string(&self.path).await?;
56        let mut pyproject_toml: DocumentMut = pyproject_toml_raw.parse::<DocumentMut>()?;
57        if pyproject_toml.get("project").is_none() {
58            pyproject_toml["project"] = toml_edit::Item::Table(toml_edit::Table::new());
59        }
60        pyproject_toml["project"]["version"] = next_version.clone().into();
61        write(
62            &self.path,
63            format!(
64                "{}{}",
65                pyproject_toml.to_string().trim_end(),
66                if pyproject_toml_raw.ends_with("\n") {
67                    "\n"
68                } else {
69                    ""
70                }
71            ),
72        )
73        .await?;
74        self.version = Some(next_version);
75        Ok(())
76    }
77
78    fn language(&self) -> Language {
79        Language::Python
80    }
81
82    fn is_changed(&self) -> bool {
83        self.is_changed
84    }
85
86    fn set_changed(&mut self, changed: bool) {
87        self.is_changed = changed;
88    }
89
90    fn relative_path(&self) -> &Path {
91        &self.relative_path
92    }
93
94    fn default_publish_command(&self) -> &'static str {
95        "uv publish"
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102    use changepacks_core::UpdateType;
103    use std::fs;
104    use tempfile::TempDir;
105    use tokio::fs::read_to_string;
106
107    #[tokio::test]
108    async fn test_python_workspace_new() {
109        let workspace = PythonWorkspace::new(
110            Some("test-workspace".to_string()),
111            Some("1.0.0".to_string()),
112            PathBuf::from("/test/pyproject.toml"),
113            PathBuf::from("test/pyproject.toml"),
114        );
115
116        assert_eq!(workspace.name(), Some("test-workspace"));
117        assert_eq!(workspace.version(), Some("1.0.0"));
118        assert_eq!(workspace.path(), PathBuf::from("/test/pyproject.toml"));
119        assert_eq!(
120            workspace.relative_path(),
121            PathBuf::from("test/pyproject.toml")
122        );
123        assert_eq!(workspace.language(), Language::Python);
124        assert_eq!(workspace.is_changed(), false);
125        assert_eq!(workspace.default_publish_command(), "uv publish");
126    }
127
128    #[tokio::test]
129    async fn test_python_workspace_new_without_name_and_version() {
130        let workspace = PythonWorkspace::new(
131            None,
132            None,
133            PathBuf::from("/test/pyproject.toml"),
134            PathBuf::from("test/pyproject.toml"),
135        );
136
137        assert_eq!(workspace.name(), None);
138        assert_eq!(workspace.version(), None);
139    }
140
141    #[tokio::test]
142    async fn test_python_workspace_set_changed() {
143        let mut workspace = PythonWorkspace::new(
144            Some("test-workspace".to_string()),
145            Some("1.0.0".to_string()),
146            PathBuf::from("/test/pyproject.toml"),
147            PathBuf::from("test/pyproject.toml"),
148        );
149
150        assert_eq!(workspace.is_changed(), false);
151        workspace.set_changed(true);
152        assert_eq!(workspace.is_changed(), true);
153        workspace.set_changed(false);
154        assert_eq!(workspace.is_changed(), false);
155    }
156
157    #[tokio::test]
158    async fn test_python_workspace_update_version_with_existing_project() {
159        let temp_dir = TempDir::new().unwrap();
160        let pyproject_toml = temp_dir.path().join("pyproject.toml");
161        fs::write(
162            &pyproject_toml,
163            r#"[tool.uv.workspace]
164members = ["packages/*"]
165
166[project]
167name = "test-workspace"
168version = "1.0.0"
169"#,
170        )
171        .unwrap();
172
173        let mut workspace = PythonWorkspace::new(
174            Some("test-workspace".to_string()),
175            Some("1.0.0".to_string()),
176            pyproject_toml.clone(),
177            PathBuf::from("pyproject.toml"),
178        );
179
180        workspace.update_version(UpdateType::Patch).await.unwrap();
181
182        let content = read_to_string(&pyproject_toml).await.unwrap();
183        assert!(content.contains("version = \"1.0.1\""));
184
185        temp_dir.close().unwrap();
186    }
187
188    #[tokio::test]
189    async fn test_python_workspace_update_version_without_project_section() {
190        let temp_dir = TempDir::new().unwrap();
191        let pyproject_toml = temp_dir.path().join("pyproject.toml");
192        fs::write(
193            &pyproject_toml,
194            r#"[tool.uv.workspace]
195members = ["packages/*"]
196"#,
197        )
198        .unwrap();
199
200        let mut workspace = PythonWorkspace::new(
201            Some("test-workspace".to_string()),
202            None,
203            pyproject_toml.clone(),
204            PathBuf::from("pyproject.toml"),
205        );
206
207        workspace.update_version(UpdateType::Patch).await.unwrap();
208
209        let content = read_to_string(&pyproject_toml).await.unwrap();
210        assert!(content.contains("[project]"));
211        assert!(content.contains("version = \"0.0.1\""));
212
213        temp_dir.close().unwrap();
214    }
215
216    #[tokio::test]
217    async fn test_python_workspace_update_version_minor() {
218        let temp_dir = TempDir::new().unwrap();
219        let pyproject_toml = temp_dir.path().join("pyproject.toml");
220        fs::write(
221            &pyproject_toml,
222            r#"[tool.uv.workspace]
223members = ["packages/*"]
224
225[project]
226name = "test-workspace"
227version = "1.0.0"
228"#,
229        )
230        .unwrap();
231
232        let mut workspace = PythonWorkspace::new(
233            Some("test-workspace".to_string()),
234            Some("1.0.0".to_string()),
235            pyproject_toml.clone(),
236            PathBuf::from("pyproject.toml"),
237        );
238
239        workspace.update_version(UpdateType::Minor).await.unwrap();
240
241        let content = read_to_string(&pyproject_toml).await.unwrap();
242        assert!(content.contains("version = \"1.1.0\""));
243
244        temp_dir.close().unwrap();
245    }
246
247    #[tokio::test]
248    async fn test_python_workspace_update_version_major() {
249        let temp_dir = TempDir::new().unwrap();
250        let pyproject_toml = temp_dir.path().join("pyproject.toml");
251        fs::write(
252            &pyproject_toml,
253            r#"[tool.uv.workspace]
254members = ["packages/*"]
255
256[project]
257name = "test-workspace"
258version = "1.0.0"
259"#,
260        )
261        .unwrap();
262
263        let mut workspace = PythonWorkspace::new(
264            Some("test-workspace".to_string()),
265            Some("1.0.0".to_string()),
266            pyproject_toml.clone(),
267            PathBuf::from("pyproject.toml"),
268        );
269
270        workspace.update_version(UpdateType::Major).await.unwrap();
271
272        let content = read_to_string(&pyproject_toml).await.unwrap();
273        assert!(content.contains("version = \"2.0.0\""));
274
275        temp_dir.close().unwrap();
276    }
277}