Skip to content

feat: version Python projects through pyproject.toml - #30

Merged
philasmar merged 1 commit into
devfrom
asmarp/pyproject-versioning
Sep 30, 2026
Merged

philasmar merged 1 commit into
devfrom
asmarp/pyproject-versioning

Conversation

@philasmar

Copy link
Copy Markdown
Owner

AutoVer can version .csproj, .nuspec and Dockerfile projects, but not Python ones. A Python package in a repository on AutoVer's release flow today has to derive its version from git tags instead (e.g. hatch-vcs with VersionFromTag). That works, but it comes with rules that are easy to get wrong:

  • it has to publish from the tag pipeline, because the release tag sits one commit behind the branch tip;
  • it needs full-depth clones;
  • stray v* tags have to be filtered out;
  • every build needs a git checkout.

This adds a pyproject.toml project type, so a Python package carries a static version that AutoVer bumps like any other project file:

{ "Projects": [ { "Name": "my-package", "Path": "pyproject.toml" } ] }

Behaviour

  • Only version in the [project] table (PEP 621) is rewritten. The file is edited line by line, not re-serialized, so quoting, spacing, comments, key order and line endings are kept. A version is seeded after name when there is none, like the other project types. A UTF-8 BOM is not kept.
  • Explicit-only. A pyproject.toml is never auto-discovered. Many repositories keep one purely for tool settings (black, ruff, pytest), and discovering those would start versioning a lint config in repositories that rely on discovery today.
  • Refused with an explanation, rather than guessed at:
    • "version" listed in dynamic;
    • a [tool.poetry]-only project;
    • a version that isn't a single-line string;
    • a normalized spelling AutoVer can't increment (1.2.3b1 gets a suggestion to write 1.2.3-b1);
    • TOML with a value that is never closed.
  • The version must be a valid Python pre-release or release (PEP 440). Labels may be alpha/a, beta/b, rc/c/pre/preview or dev, optionally numbered. beta.1 writes 1.2.3-beta.1, which pip reads as 1.2.3b1.
    • hotfix is rejected because it would make a package pip refuses to install.
    • post.1 or 1 are rejected because they are post-releases, which pip ranks above 1.2.3 and installs by default.
    • The check matches the packaging library pip uses: on a 27-label table, and on about 30k fuzzed labels.
  • Checked before any file is written. VersionCommand now plans every update, validates them all through a new IProjectFileHandler.ValidateVersion (a no-op by default), and only then writes and stages. A version a pyproject can't take fails the release without leaving other projects bumped.
  • Config-loading errors now say why. "There was an issue loading the user configuration at …" now ends with the underlying cause: a missing project file, a pyproject that can't be versioned, or the JSON path of a bad setting. Before, it named only autover.json.

The TOML scanner handles:

  • basic strings with escapes;
  • multi-line strings, including escaped quotes and 4–5 quote closings;
  • nested arrays and inline tables, with brackets inside strings such as "requests[socks]>=2";
  • comments;
  • dotted keys;
  • [project.*] sub-tables and [[array.of.tables]];
  • CRLF files and files without a trailing newline.

Compatibility

No change for existing projects. The pre-change and new builds were run side by side on 36 scripted repositories: csproj, nuspec, Dockerfile, auto-discovery, VersionFromTag, change files, every version / changelog / change option, and error cases. Successful runs were identical in exit codes, stdout, files, commits and tags. The differences are all on error paths:

  • config-loading errors append their cause (same exit code, same stream, old text kept as a prefix);
  • file-type lists in messages mention pyproject.toml;
  • when a later project fails while writing (e.g. a nuspec with no <metadata>), an earlier file is left bumped but unstaged rather than staged. No commit or tag is made in either case.

IProjectHandler gains ValidateVersion. AutoVer ships only as a dotnet tool, and ProjectHandler is its only implementation.

Tests

  • PyprojectFileHandlerTest (66): matching, reading and every TOML case above, each refusal, writing (byte-for-byte), seeding positions, and Python version rules.
  • PyprojectVersionCommandTests (10), through the CLI:
    • a change file bumps, commits and tags;
    • --current;
    • seeding with InitialVersion;
    • a dynamic version fails without touching the repository;
    • no auto-discovery;
    • a shared version with a csproj;
    • changelog;
    • a pip-rejected version fails before the csproj is written;
    • 1.2.3b1 gives an explained error rather than an unhandled exception;
    • JSON paths in config errors.
  • Every fix found in review was mutation-checked: the test fails with the fix removed.
  • Written files were checked with Python's tomllib on 36 edge-case inputs: all parse, carry the intended version, and are otherwise identical.
  • End to end: a real package bumped from 0.1.0 to 0.2.0 as a one-line diff, and its wheel builds as …-0.2.0-py3-none-any.whl.

Full suite: 113 integration, 256 unit, 0 failures (from 103 / 190). The clean build has no warnings.

@philasmar
philasmar merged commit 6d9d6f8 into dev Sep 30, 2026
2 checks passed
@philasmar
philasmar deleted the asmarp/pyproject-versioning branch September 30, 2026 15:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant