xtask/
release_versions_python.rs1use anyhow::{Context, Result, bail};
2
3pub(super) fn read_pyproject_version(content: &str, package: Option<&str>) -> Result<String> {
4 let value: toml::Value = toml::from_str(content).context("invalid TOML")?;
5 let table = value
6 .get("project")
7 .and_then(toml::Value::as_table)
8 .context("missing [project] table")?;
9 if let Some(expected_name) = package {
10 let name = table
11 .get("name")
12 .and_then(toml::Value::as_str)
13 .context("missing project.name")?;
14 if name != expected_name {
15 bail!("expected Python project {expected_name}, found {name}");
16 }
17 }
18 table
19 .get("version")
20 .and_then(toml::Value::as_str)
21 .map(ToOwned::to_owned)
22 .context("missing project.version")
23}
24
25fn python_assignment_value<'a>(line: &'a str, variable: &str) -> Option<&'a str> {
26 line.trim_start()
27 .strip_prefix(variable)?
28 .trim_start()
29 .strip_prefix('=')
30 .map(str::trim_start)
31}
32
33pub(super) fn read_python_assignment_version(
34 content: &str,
35 variable: Option<&str>,
36) -> Result<String> {
37 let variable = variable.context("python_assignment requires variable")?;
38 let value = content
39 .lines()
40 .find_map(|line| python_assignment_value(line, variable))
41 .with_context(|| format!("missing Python assignment {variable}"))?;
42 let value = value.split('#').next().unwrap_or(value).trim();
43 for quote in ['"', '\''] {
44 if let Some(value) = value
45 .strip_prefix(quote)
46 .and_then(|value| value.strip_suffix(quote))
47 {
48 return Ok(value.to_owned());
49 }
50 }
51 bail!("Python assignment {variable} must be a quoted string")
52}
53
54pub(super) fn replace_pyproject_version(
55 content: &str,
56 package: Option<&str>,
57 next: &str,
58) -> Result<String> {
59 read_pyproject_version(content, package)?;
60 let mut in_project = false;
61 let mut replaced = false;
62 let mut output = Vec::new();
63 for line in content.lines() {
64 let trimmed = line.trim();
65 if trimmed == "[project]" {
66 in_project = true;
67 } else if in_project && trimmed.starts_with('[') {
68 in_project = false;
69 }
70
71 let mut next_line = line.to_owned();
72 if in_project && trimmed.starts_with("version = ") {
73 let leading = &line[..line.len() - line.trim_start().len()];
74 next_line = format!(r#"{leading}version = "{next}""#);
75 replaced = true;
76 }
77 output.push(next_line);
78 }
79 if !replaced {
80 bail!("missing Python project version");
81 }
82 Ok(preserve_trailing_newline(content, output.join("\n")))
83}
84
85pub(super) fn replace_python_assignment_version(
86 content: &str,
87 variable: Option<&str>,
88 next: &str,
89) -> Result<String> {
90 let variable = variable.context("python_assignment requires variable")?;
91 read_python_assignment_version(content, Some(variable))?;
92 let mut replaced = false;
93 let output = content
94 .lines()
95 .map(|line| {
96 if !replaced && python_assignment_value(line, variable).is_some() {
97 replaced = true;
98 let leading = &line[..line.len() - line.trim_start().len()];
99 format!(r#"{leading}{variable} = "{next}""#)
100 } else {
101 line.to_owned()
102 }
103 })
104 .collect::<Vec<_>>()
105 .join("\n");
106 if !replaced {
107 bail!("missing Python assignment {variable}");
108 }
109 Ok(preserve_trailing_newline(content, output))
110}
111
112fn preserve_trailing_newline(original: &str, mut output: String) -> String {
113 if original.ends_with('\n') {
114 output.push('\n');
115 }
116 output
117}