Skip to content

fix(providers/uv): treat uv.lock as optional#1979

Open
bearomorphism wants to merge 2 commits intocommitizen-tools:masterfrom
bearomorphism:fix/1383-uv-lock-optional
Open

fix(providers/uv): treat uv.lock as optional#1979
bearomorphism wants to merge 2 commits intocommitizen-tools:masterfrom
bearomorphism:fix/1383-uv-lock-optional

Conversation

@bearomorphism
Copy link
Copy Markdown
Collaborator

@bearomorphism bearomorphism commented May 9, 2026

Description

Closes #1383.

Why

UvProvider.set_lock_version() unconditionally read uv.lock via self.lock_file.read_text(), so cz bump crashed with FileNotFoundError: [Errno 2] No such file or directory: 'uv.lock' whenever the lock had not been written yet. Two real-world reproducers:

  1. A freshly initialised uv project that has never had uv sync run (the original reporter's reproduction, posted in the issue thread).
  2. A uv workspace member: the lock lives at the workspace root, not next to the package's own pyproject.toml. A user reported this in a follow-up comment using uv 0.7.9 / cz 4.8.0.

The maintainer (@Lee-W) confirmed in the issue thread: "hmmm... I thought we have it long ago. yep, we'll definitely need it."

What changed

File Change
commitizen/providers/uv_provider.py set_version() now wraps the set_lock_version(version) call in if self.lock_file.is_file():, mirroring the convention used by cargo_provider.py:42 and npm_provider.py:53,62. set_lock_version() itself is unchanged so external callers that already verified the lock exists keep working.
tests/providers/test_uv_provider.py New test_uv_provider_without_lock_file — creates a pyproject.toml without a sibling uv.lock, runs set_version("100.100.100"), asserts pyproject.toml was updated and no uv.lock was created.

How it works

  • The guard uses Path.is_file(), not Path.exists(). exists() is also true for directories and symlinks-to-non-files, both of which would still trip read_text(). The cargo and npm providers already make this distinction; the uv provider was the outlier.
  • The guard is hoisted to set_version() rather than placed at the top of set_lock_version(). This matches cargo_provider.py's structure and means the lock-rewrite path can keep its precondition: "if you call set_lock_version, the lock must exist."
  • For uv workspace members the lock is at the workspace root, not next to the package. The lock_file property here resolves to the cwd-relative uv.lock, which won't exist for a workspace member running from its own subdirectory; the is_file() guard skips the rewrite cleanly. A future PR could add proper workspace-root resolution, but that's a feature, not part of this fix — uv reconciles the lock on the next uv sync either way.
  • TOCTOU race (lock created between is_file() and read_text()) is benign — the subsequent read_text() would simply succeed with the new content.

Backward compatibility

  • Existing path (lock present, single-package uv project) is exercised by the unchanged test_uv_provider[hyphenated|underscore] and produces byte-identical output.
  • No public API change; set_lock_version() keeps the same signature.

Checklist

Was generative AI tooling used to co-author this PR?

  • Yes (please specify the tool below)

Generated-by: Claude following the guidelines

Code Changes

  • Add test cases to all the changes you introduce (1 new regression test)
  • Run uv run poe all locally to ensure this change passes linter check and tests (poe lint clean; 3/3 uv_provider tests pass — 2 existing + 1 new)
  • Manually test the changes (see "Steps to Test" below for the issue reporter's exact reproduction script)
  • Update the documentation for the changes (no doc change required — the lock-handling behaviour is internal)

Expected Behavior

Scenario Before this PR After this PR
Fresh uv project, no uv.lock yet cz bump crashes with FileNotFoundError: 'uv.lock' pyproject.toml is updated, no lock written, exit 0.
uv workspace member running from subdirectory Same FileNotFoundError Same — package's pyproject.toml updated, lock-rewrite skipped (workspace root will be reconciled by next uv sync).
Single-package project with a populated uv.lock Both files updated Both files updated (unchanged).
uv.lock is a directory or symlink to a non-file Crashes with the same error Skipped via is_file() (matches cargo/npm convention).

Steps to Test This Pull Request

git fetch fork fix/1383-uv-lock-optional
git checkout fork/fix/1383-uv-lock-optional

# 1. Targeted regression test (3 cases).
uv run pytest tests/providers/test_uv_provider.py -v
# expected: 3 passed

# 2. Reproduce the original bug, then verify the fix (matches the issue reporter's script).
mkdir -p /tmp/cz-1383 && cd /tmp/cz-1383
git init -q
uv init --quiet
uv run cz init --no-prompt   # or follow the prompts; pick `uv` as the version provider
sed -i.bak 's/pep621/uv/' pyproject.toml
git add . && git -c user.email=a@b -c user.name=t commit -qm "feat: a"
# On master this would now crash. On this PR it should succeed:
uv run cz bump --yes
test -f uv.lock && echo "lock exists" || echo "no lock — expected"
grep '^version' pyproject.toml   # should show the bumped version

Additional Context

Surfaced while triaging open issues in #1976 (round 3). Switched from Path.exists() to Path.is_file() in fixup commit f204942e after GitHub Copilot's review noted that the existing convention in cargo_provider.py:42 and npm_provider.py:53,62 is is_file() (and that exists() would still trip if the lock path happened to be a directory). The issue had been open for 11 months and was already labelled good first issue + wait-for-implementation; the previously assigned contributor confirmed they were happy for someone else to take it over.

Closes commitizen-tools#1383.

`UvProvider.set_lock_version()` unconditionally read `uv.lock`, which
made `cz bump` crash with `FileNotFoundError` whenever the lock had not
been written yet. The two reproducers in commitizen-tools#1383 are:

- a freshly initialised uv project (`uv init`) before `uv sync` has run,
- a uv workspace member where the lock lives at the workspace root, not
  alongside the package's own `pyproject.toml`.

Add an `if not self.lock_file.exists(): return` early-out so updating
`pyproject.toml` is enough; the lock is rewritten only when present.

A regression test creates a `pyproject.toml` without a sibling `uv.lock`
and asserts that `set_version` updates the project file and does not
write a lock.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@codecov
Copy link
Copy Markdown

codecov Bot commented May 9, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.23%. Comparing base (4b93a50) to head (f204942).
⚠️ Report is 3 commits behind head on master.

Additional details and impacted files
@@           Coverage Diff           @@
##           master    #1979   +/-   ##
=======================================
  Coverage   98.23%   98.23%           
=======================================
  Files          61       61           
  Lines        2779     2780    +1     
=======================================
+ Hits         2730     2731    +1     
  Misses         49       49           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes cz bump crashing for uv projects when uv.lock is absent by treating the lockfile as optional and adding a regression test for the missing-lock scenario.

Changes:

  • Add an early return in UvProvider.set_lock_version() when uv.lock is missing.
  • Add a regression test ensuring bumping updates pyproject.toml and does not error when no uv.lock exists.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
commitizen/providers/uv_provider.py Skip lock rewrite when uv.lock is not present to avoid FileNotFoundError.
tests/providers/test_uv_provider.py Add regression coverage for projects without a lockfile.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread commitizen/providers/uv_provider.py Outdated
Comment on lines +29 to +33
if not self.lock_file.exists():
# `uv.lock` is optional: a freshly initialised project (or a uv
# workspace member, since the lock lives at the workspace root)
# may not have one yet. Updating `pyproject.toml` is enough.
return
Address GitHub Copilot review feedback on PR commitizen-tools#1979: switch the
optional-lock guard from `Path.exists()` to `Path.is_file()` and
hoist it from `set_lock_version()` to `set_version()`. This matches
the existing pattern in `commitizen/providers/cargo_provider.py:42`
and `commitizen/providers/npm_provider.py:53,62`, and also avoids
crashing if `uv.lock` happens to be a directory or symlink to a
non-file (`exists()` is true for both, `is_file()` is not).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Make uv.lock optional bump

2 participants