Repository navigation
fix: make _sub_once's duplicate guard able to fire (#65) - #85
Conversation
fixes #65 re.subn was called with count=1, which caps the REPORTED substitution count at 1 — so `n != 1` could only ever catch zero matches. A second occurrence of the pattern (a version string quoted twice in the README, say) would be left un-edited and ship in a release the bumper reported as successful. Dropping the cap makes the guard mean what it says. Removing it is safe because every edit is computed before any file is written, so a duplicate aborts with nothing touched — same reasoning as the fix praxen landed. Adds tests/test_bump_version.py: single match substitutes, zero matches abort, duplicate aborts. Negative-verified — the duplicate case fails red against the old count=1 version. Found while porting the script to praxen (praxen#233 review). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Steve Wilson <steve.wilson@exabeam.com>
mattwillems-exabeam
left a comment
There was a problem hiding this comment.
Approve. Verified independently rather than taking the description's word for it — everything in it holds.
Confirmed
- The defect is real.
count=1caps whatre.subnreports, soncould never exceed 1 andn != 1could only ever fire on zero. The duplicate branch was unreachable. - The safety argument checks out in the source, which is the part that mattered. Removing the cap means a duplicate substitutes twice in the returned string — harmless only if nothing is written first.
_sub_onceruns during construction of theeditslist (bump_version.py:63), the write loop is:78, andfail()raisesSystemExit(:40). So a duplicate aborts with no file touched. Had that ordering been the other way round, this fix would have turned a silent miss into a half-written release. - Negative-verified myself, not just read: restoring
count=1makestest_duplicate_match_abortsfail with DID NOT RAISE, and the other two still pass. The test genuinely pins the change. - 268 pass,
--dry-runresolves both edit targets plus the BOM regen.
Two things I checked that aren't in the description
- No other instance of the pattern. Every other
re.sub/subnin the repo is an uncapped replace-all with no count-based guard, so_sub_oncewas the only place this class could hide. - Nothing currently trips the now-live guard. Only two files carry a version —
plugin/.claude-plugin/plugin.jsonandplugin/README.md— the bumper edits both, each matches exactly once, and the thin root README has no pill to drift.test_readme_version_badge_matches_pluginalso points atplugin/README.mdpost-#66. So making the guard fire doesn't newly block the release path.
One sequencing note — not a change request
This touches no shipped file (scripts/ and tests/ both stay out of the distribution), so merging it has zero effect on what users receive. But dev is an open release candidate: #83 is cut, the 0.7.0 CHANGELOG entry is written, and it says fixes #29 #70 #76. Landing this into dev closes #65 in 0.7.0 with nothing in the changelog describing it.
Same applies to my #84, so I'm not pointing at yours — it's a question about both:
- fold both into 0.7.0 and add a line each to the existing entry, or
- hold both for 0.7.1 and let the candidate stay as cut.
Either is fine; silently drifting the candidate away from its own release notes is the option worth avoiding. Your call as release author — say which and I'll add the changelog lines or hold the merges accordingly.
fixes #65
The defect
_sub_oncecalledre.subn(pattern, repl, text, count=1). Thecountcap limits how many substitutions are made, and therefore how many are reported — soncan never exceed 1, and then != 1guard could only ever catch the zero-match case. The duplicate case it was written for was unreachable.The consequence isn't theoretical: if a version string ever appeared twice in
plugin/README.md, the first would be bumped, the second would silently survive, and the bumper would report success. The post-bump verify checks the pill it just edited, so it wouldn't necessarily catch the stale second copy either.The fix
Drop the cap and let the guard mean what it says — the same fix praxen landed in its port of this script.
Why removing the cap is safe rather than newly destructive: without it, two matches means both get substituted in the returned string — but that string is never written. Every edit is computed into the
editslist before the write loop runs, andfail()raisesSystemExit, so a duplicate aborts the release with no file touched. I verified that ordering in the source rather than assuming it.Tests
New
tests/test_bump_version.py— there was no coverage of this script at all:found 2.Negative-verified: stashing the fix and re-running makes the duplicate test fail red, so it genuinely exercises the change rather than passing vacuously.
Verification
268 tests pass (265 + 3 new) · AI BOM current ·
bump_version.py 0.7.1 --dry-runstill resolves all three edit targets, so the release path is unaffected.Found while porting the script to praxen (praxen#233 review).
🤖 Generated with Claude Code