Skip to main content

xtask/
release_versions_python.rs

1use 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}