use crate::toml_helpers;
use crate::version_file::{VersionFile, VersionFileError};
const SECTION: &str = "[project]";
#[derive(Debug, Clone, Copy)]
pub struct PyprojectVersionFile;
impl VersionFile for PyprojectVersionFile {
fn name(&self) -> &str {
"pyproject.toml"
}
fn filenames(&self) -> &[&str] {
&["pyproject.toml"]
}
fn detect(&self, content: &str) -> bool {
toml_helpers::detect_version_in_section(content, SECTION)
}
fn read_version(&self, content: &str) -> Option<String> {
toml_helpers::read_version_in_section(content, SECTION)
}
fn write_version(&self, content: &str, new_version: &str) -> Result<String, VersionFileError> {
toml_helpers::write_version_in_section(content, SECTION, new_version)
}
}
#[cfg(test)]
mod tests {
use super::*;
const BASIC_PYPROJECT: &str = r#"[project]
name = "my-package"
version = "0.1.0"
description = "A test package"
"#;
const MULTI_SECTION_PYPROJECT: &str = r#"[project]
name = "my-package"
version = "0.1.0"
description = "A test package"
[tool.poetry]
version = "0.1.0"
"#;
#[test]
fn detect_with_project_version() {
assert!(PyprojectVersionFile.detect(BASIC_PYPROJECT));
}
#[test]
fn detect_without_project_section() {
let content = "[tool.poetry]\nversion = \"1.0.0\"\n";
assert!(!PyprojectVersionFile.detect(content));
}
#[test]
fn detect_project_without_version() {
let content = "[project]\nname = \"x\"\n\n[tool.poetry]\nversion = \"1.0.0\"\n";
assert!(!PyprojectVersionFile.detect(content));
}
#[test]
fn read_version_basic() {
assert_eq!(
PyprojectVersionFile.read_version(BASIC_PYPROJECT),
Some("0.1.0".to_string()),
);
}
#[test]
fn read_version_no_project() {
let content = "[tool.poetry]\nversion = \"1.0.0\"\n";
assert_eq!(PyprojectVersionFile.read_version(content), None);
}
#[test]
fn write_version_basic() {
let result = PyprojectVersionFile
.write_version(BASIC_PYPROJECT, "1.0.0")
.unwrap();
assert!(result.contains("version = \"1.0.0\""));
assert!(result.contains("name = \"my-package\""));
assert!(result.contains("description = \"A test package\""));
}
#[test]
fn write_version_only_in_project_section() {
let result = PyprojectVersionFile
.write_version(MULTI_SECTION_PYPROJECT, "2.0.0")
.unwrap();
assert!(result.contains("version = \"2.0.0\""));
let count = result.matches("version = \"0.1.0\"").count();
assert_eq!(count, 1, "tool.poetry version should remain 0.1.0");
}
#[test]
fn write_version_no_field_returns_error() {
let content = "[project]\nname = \"x\"\n";
let err = PyprojectVersionFile.write_version(content, "1.0.0");
assert!(err.is_err());
}
#[test]
fn write_version_preserves_no_trailing_newline() {
let content = "[project]\nname = \"x\"\nversion = \"0.1.0\"";
let result = PyprojectVersionFile
.write_version(content, "0.2.0")
.unwrap();
assert!(!result.ends_with('\n'));
assert!(result.contains("version = \"0.2.0\""));
}
#[test]
fn integration_with_tempdir() {
use std::fs;
let dir = tempfile::tempdir().unwrap();
let pyproject = dir.path().join("pyproject.toml");
fs::write(
&pyproject,
r#"[project]
name = "example"
version = "0.1.0"
requires-python = ">=3.8"
[tool.setuptools]
packages = ["example"]
"#,
)
.unwrap();
let content = fs::read_to_string(&pyproject).unwrap();
assert!(PyprojectVersionFile.detect(&content));
assert_eq!(
PyprojectVersionFile.read_version(&content),
Some("0.1.0".to_string()),
);
let updated = PyprojectVersionFile
.write_version(&content, "2.0.0")
.unwrap();
fs::write(&pyproject, &updated).unwrap();
let on_disk = fs::read_to_string(&pyproject).unwrap();
assert!(on_disk.contains("version = \"2.0.0\""));
assert!(on_disk.contains("name = \"example\""));
assert!(on_disk.contains("requires-python = \">=3.8\""));
}
}