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