fix(providers/uv): treat uv.lock as optional#1979
Conversation
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 Report✅ All modified and coverable lines are covered by tests. 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. |
There was a problem hiding this comment.
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()whenuv.lockis missing. - Add a regression test ensuring bumping updates
pyproject.tomland does not error when nouv.lockexists.
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.
| 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>
| def set_version(self, version: str) -> None: | ||
| super().set_version(version) | ||
| self.set_lock_version(version) | ||
| # `uv.lock` is optional: a freshly initialised project (or a uv |
There was a problem hiding this comment.
| # `uv.lock` is optional: a freshly initialised project (or a uv | |
| # `uv.lock` is optional: a freshly intialized project (or a uv |
I think we tend to use the US form. but TBH, i don't care as much
Description
Closes #1383.
Why
UvProvider.set_lock_version()unconditionally readuv.lockviaself.lock_file.read_text(), socz bumpcrashed withFileNotFoundError: [Errno 2] No such file or directory: 'uv.lock'whenever the lock had not been written yet. Two real-world reproducers:uv syncrun (the original reporter's reproduction, posted in the issue thread).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
commitizen/providers/uv_provider.pyset_version()now wraps theset_lock_version(version)call inif self.lock_file.is_file():, mirroring the convention used bycargo_provider.py:42andnpm_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.pytest_uv_provider_without_lock_file— creates apyproject.tomlwithout a siblinguv.lock, runsset_version("100.100.100"), assertspyproject.tomlwas updated and nouv.lockwas created.How it works
Path.is_file(), notPath.exists().exists()is also true for directories and symlinks-to-non-files, both of which would still tripread_text(). The cargo and npm providers already make this distinction; the uv provider was the outlier.set_version()rather than placed at the top ofset_lock_version(). This matchescargo_provider.py's structure and means the lock-rewrite path can keep its precondition: "if you callset_lock_version, the lock must exist."lock_fileproperty here resolves to the cwd-relativeuv.lock, which won't exist for a workspace member running from its own subdirectory; theis_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 nextuv synceither way.is_file()andread_text()) is benign — the subsequentread_text()would simply succeed with the new content.Backward compatibility
test_uv_provider[hyphenated|underscore]and produces byte-identical output.set_lock_version()keeps the same signature.Checklist
Was generative AI tooling used to co-author this PR?
Generated-by: Claude following the guidelines
Code Changes
uv run poe alllocally to ensure this change passes linter check and tests (poe lintclean; 3/3 uv_provider tests pass — 2 existing + 1 new)Expected Behavior
uv.lockyetcz bumpcrashes withFileNotFoundError: 'uv.lock'pyproject.tomlis updated, no lock written, exit 0.FileNotFoundErrorpyproject.tomlupdated, lock-rewrite skipped (workspace root will be reconciled by nextuv sync).uv.lockuv.lockis a directory or symlink to a non-fileis_file()(matches cargo/npm convention).Steps to Test This Pull Request
Additional Context
Surfaced while triaging open issues in #1976 (round 3). Switched from
Path.exists()toPath.is_file()in fixup commitf204942eafter GitHub Copilot's review noted that the existing convention incargo_provider.py:42andnpm_provider.py:53,62isis_file()(and thatexists()would still trip if the lock path happened to be a directory). The issue had been open for 11 months and was already labelledgood first issue+wait-for-implementation; the previously assigned contributor confirmed they were happy for someone else to take it over.