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