fix(version): recognize PEP 440 development tags - #2026
Conversation
Keep development-release tags discoverable across sequential bumps while preserving custom tag embedding and existing suffix behavior. Constraint: Preserve custom tag formats and SemVer-compatible parsing. Rejected: Replace the shared parser with the anchored PEP 440 reference regex | it breaks embedded tag formats and unrelated schemes. Confidence: high Scope-risk: narrow Directive: Keep generated version forms round-trippable through TagRules. Tested: uv run poe all; 446 focused tag, version, bump, and changelog tests; exact uv-provider sequential bump regression; git diff --check. Not-tested: Full tox matrix across every supported Python version and operating system.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #2026 +/- ##
=======================================
Coverage 98.23% 98.23%
=======================================
Files 61 61
Lines 2784 2784
=======================================
Hits 2735 2735
Misses 49 49 ☔ View full report in Codecov by Harness. |
Manny7717
left a comment
There was a problem hiding this comment.
Verified locally against head bca897506ad1242431ac6365347f62fd694015e0 (commitizen master + this PR's 3 files):
- Regression proven: on master's code,
TagRules().is_version_tag("0.2.0.dev0")is False andextract_versionraisesInvalidVersion— same for0.2.0.dev0+build.1,0.2.0a1.dev0,0.2.0a1.dev0+build.1,2.0.0.dev0, and1.2.3rc1+build5. With this PR all parse correctly; the newtest_is_version_tag_accepts_pep440_devreleasefails on master and passes here. - No regressions (differential battery, 15 tags incl.
1.2.3,v1.2.3,1.2.3rc1,1.2.3-dev1,1.2.3+build.1,0.2.0dev0,0.1.0): every tag recognized on master is recognized identically here;0.2.0.not-a-releasestays rejected. The change is strictly additive — it only accepts previously-rejected valid PEP 440 forms. - Group numbering unchanged (
(\w+)?→(?:\w+)?, build group was never captured), so nothing referencing the old groups breaks. - Tests:
tests/test_tags.py+tests/test_version_schemes.py= 16 passed;test_bump_command.py::test_bump_devrelease_with_incremental_changelogpasses (exercises two realbump --devreleaseruns incl. uv-provider version sync + incremental changelog); ruff clean on all touched files.
One non-blocking nit: the dotless forms (0.2.0dev0) normalize correctly via packaging, but the new test's 0.2.0.dev0+build.1 round-trip depends on str(Version) normalization — worth keeping in mind if a custom tag_format ever needs the literal string preserved. Not a blocker.
Fix is correct, minimal, and well-tested — approving.
Manny7717
left a comment
There was a problem hiding this comment.
Verified on head bca8975 vs base origin/master 4184174.
Bug real on base. The default version parser regex placed (?:\+[0-9A-Za-z.]+)?(\w+)? at the end, so a .devN suffix after a -prerelease-free version was never consumed: for tag 0.2.0.dev0 the regex matched only 0.2.0 and left .dev0 dangling. TagRules.is_version_tag therefore rejected the very dev tags cz bump --devrelease itself creates, breaking the next incremental changelog (previous tag not found). Reproduced: test_is_version_tag_accepts_pep440_devrelease and test_bump_devrelease_with_incremental_changelog FAIL on base (assertion: is_version_tag('0.2.0.dev0') is False / second bump couldn't find 0.2.0.dev1), PASS on head.
Fix correct. (?:\.dev\d+)? inserted before the +build clause; the loose (\w+)? moved earlier. I diffed old vs new parser behavior over a 23-tag matrix (dev, dotless-dev, dev+build, a1.dev0, rc, post, local, custom formats, junk like .not-a-release and 1.2.3.4): every string the old parser fully consumed is consumed identically by the new one, and the new parser additionally accepts exactly the PEP 440 dev-release family (0.2.0.dev0, 0.2.0.dev0+build.1, 0.2.0a1.dev0+build.1, 2.0.0.dev5+g1234, 0.1.0.dev5+abc) plus 1.2.3rc1+meta (build metadata after a dotless prerelease, previously left unconsumed). No previously-valid shape regressed and nothing junk gained acceptance: 0.2.0.not-a-release, 1.2.3.post1, 1.2.3.4 still rejected.
Zero regressions. Full suites test_tags.py + commands/test_bump_command.py + test_version_schemes.py: head 152 passed vs base 150 passed — delta exactly the 2 new tests. ruff check + ruff format --check clean on the changed files.
One non-blocking note: dotless 1.2.3dev0 is still accepted only via the loose (\w+)? fallback, so anything word-shaped (e.g. 0.2.0x) also passes as a version — that permissiveness predates this PR and is unchanged; out of scope.
Description
Fixes #1367.
The default
$versionparser rejected.devNtags that Commitizen itself creates. This keeps generated PEP 440 prerelease, development, and local-version combinations discoverable so the next incremental changelog can find the previous tag.Checklist
Was generative AI tooling used to co-author this PR?
Generated-by: OpenAI Codex following the guidelines
Code Changes
uv-provider bumpsuv run poe alllocallyExpected Behavior
After creating
0.2.0.dev0, a subsequent--devrelease 1bump should recognize that tag and generate the0.2.0.dev1changelog, tag,pyproject.toml, anduv.lockupdates.Steps to Test This Pull Request
uvversion provider withupdate_changelog_on_bumpandchangelog_incrementalenabled.cz bump --devrelease 0 --yesafter a feature commit.cz bump --devrelease 1 --yes.0.2.0.dev1to the tag, changelog,pyproject.toml, anduv.lock.