changepacks_python/
workspace.rs1use 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}