feat(pyproject): release v1.7.0 — Python pyproject.toml version bump
Add internal/pyproject package with ReadVersion and WriteVersion for pyproject.toml files. Reads [project].version (PEP 621) first, then falls back to [tool.poetry].version. Section boundaries are detected via TOML's rule that headers always start at the beginning of a line (\n[ pattern), so inline arrays with [ characters don't interfere. Config follows the established multi-value pattern: - python.pyproject_toml: single path (opt-in, no default) - python.pyproject_tomls: list for monorepos (overrides single) - --pyproject flag overrides pyproject_toml and clears pyproject_tomls 100% per-package statement coverage maintained across all 14 packages; FuzzReadVersion and FuzzWriteVersion added per fuzzing guidelines. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
package pyproject
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
// sectionRe matches a TOML section from its header to the next header (or end of file).
|
||||
// TOML section headers always appear at the start of a line, so \n[ is a reliable boundary.
|
||||
var (
|
||||
projectSectionRe = regexp.MustCompile(`(?s)\[project\].*?(?:\n\[|\z)`)
|
||||
poetrySectionRe = regexp.MustCompile(`(?s)\[tool\.poetry\].*?(?:\n\[|\z)`)
|
||||
)
|
||||
|
||||
// versionLineRe matches version = "x.y.z" on its own line inside a section.
|
||||
var versionLineRe = regexp.MustCompile(`(?m)^version\s*=\s*"([^"]+)"`)
|
||||
|
||||
// ReadVersion returns the project version from a pyproject.toml file.
|
||||
// It checks [project] (PEP 621) first, then [tool.poetry] (Poetry).
|
||||
func ReadVersion(path string) (string, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("read %s: %w", path, err)
|
||||
}
|
||||
content := string(data)
|
||||
for _, sectionRe := range []*regexp.Regexp{projectSectionRe, poetrySectionRe} {
|
||||
section := sectionRe.FindString(content)
|
||||
if section == "" {
|
||||
continue
|
||||
}
|
||||
m := versionLineRe.FindStringSubmatch(section)
|
||||
if m != nil {
|
||||
return m[1], nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("no version found in %s", path)
|
||||
}
|
||||
|
||||
// WriteVersion replaces the project version in a pyproject.toml file in-place.
|
||||
// oldVersion must match what ReadVersion returned.
|
||||
func WriteVersion(path, oldVersion, newVersion string) error {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read %s: %w", path, err)
|
||||
}
|
||||
updated, ok := replaceVersion(string(data), oldVersion, newVersion)
|
||||
if !ok {
|
||||
return fmt.Errorf("version %q not found in %s", oldVersion, path)
|
||||
}
|
||||
return os.WriteFile(path, []byte(updated), 0644)
|
||||
}
|
||||
|
||||
// replaceVersion finds and replaces the version in the first [project] or [tool.poetry]
|
||||
// section that contains it. Returns the updated content and true on success.
|
||||
// regexp.QuoteMeta is used so version strings with dots or specials are safe.
|
||||
func replaceVersion(content, oldVersion, newVersion string) (string, bool) {
|
||||
re := regexp.MustCompile(`(?m)^(version\s*=\s*)"` + regexp.QuoteMeta(oldVersion) + `"`)
|
||||
for _, sectionRe := range []*regexp.Regexp{projectSectionRe, poetrySectionRe} {
|
||||
loc := sectionRe.FindStringIndex(content)
|
||||
if loc == nil {
|
||||
continue
|
||||
}
|
||||
section := content[loc[0]:loc[1]]
|
||||
m := re.FindStringSubmatchIndex(section)
|
||||
if m == nil {
|
||||
continue
|
||||
}
|
||||
// m[0]:m[1] = full match; m[2]:m[3] = group 1 (prefix "version = ")
|
||||
newSection := section[:m[0]] + section[m[2]:m[3]] + `"` + newVersion + `"` + section[m[1]:]
|
||||
return content[:loc[0]] + newSection + content[loc[1]:], true
|
||||
}
|
||||
return content, false
|
||||
}
|
||||
Reference in New Issue
Block a user