From 5392da8669b5afe8c924ac1366f0281bbe61ef55 Mon Sep 17 00:00:00 2001 From: Tim Hsiung Date: Sat, 9 May 2026 20:00:20 +0800 Subject: [PATCH 1/2] fix(providers/uv): treat uv.lock as optional Closes #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 #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> --- commitizen/providers/uv_provider.py | 6 ++++++ tests/providers/test_uv_provider.py | 21 +++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/commitizen/providers/uv_provider.py b/commitizen/providers/uv_provider.py index 0eb19e51a..d8cb23375 100644 --- a/commitizen/providers/uv_provider.py +++ b/commitizen/providers/uv_provider.py @@ -26,6 +26,12 @@ def set_version(self, version: str) -> None: self.set_lock_version(version) def set_lock_version(self, version: str) -> None: + 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 + pyproject_toml_content = tomlkit.parse( self.file.read_text(encoding=self._get_encoding()) ) diff --git a/tests/providers/test_uv_provider.py b/tests/providers/test_uv_provider.py index e0da97916..f47d7a6c2 100644 --- a/tests/providers/test_uv_provider.py +++ b/tests/providers/test_uv_provider.py @@ -119,3 +119,24 @@ def test_uv_provider( file_regression.check(updated_pyproject_toml_content, extension=".toml") file_regression.check(updated_uv_lock_content, extension=".lock") + + +def test_uv_provider_without_lock_file(config: BaseConfig, tmp_path, monkeypatch): + """Regression for #1383: a freshly initialised uv project (or a uv + workspace member) has no `uv.lock` yet; bumping must still update + `pyproject.toml` instead of raising `FileNotFoundError`.""" + monkeypatch.chdir(tmp_path) + pyproject_toml_file = tmp_path / UvProvider.filename + pyproject_toml_file.write_text(PYPROJECT_TOML, encoding="utf-8") + assert not (tmp_path / UvProvider.lock_filename).exists() + + config.settings["version_provider"] = "uv" + + provider = get_provider(config) + assert isinstance(provider, UvProvider) + assert provider.get_version() == "4.2.1" + + provider.set_version("100.100.100") + assert provider.get_version() == "100.100.100" + assert "100.100.100" in pyproject_toml_file.read_text(encoding="utf-8") + assert not (tmp_path / UvProvider.lock_filename).exists() From f204942e770485e94dc50c25daf1027c4c786c7b Mon Sep 17 00:00:00 2001 From: Tim Hsiung Date: Sat, 9 May 2026 20:41:16 +0800 Subject: [PATCH 2/2] fix(providers/uv): use `is_file()` to match cargo/npm convention Address GitHub Copilot review feedback on PR #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> --- commitizen/providers/uv_provider.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/commitizen/providers/uv_provider.py b/commitizen/providers/uv_provider.py index d8cb23375..f6291d6df 100644 --- a/commitizen/providers/uv_provider.py +++ b/commitizen/providers/uv_provider.py @@ -23,15 +23,16 @@ def lock_file(self) -> Path: 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 + # workspace member, since the lock lives at the workspace root) + # may not have one yet. Updating `pyproject.toml` is enough. + # Use `is_file()` (not `exists()`) to skip directories or other + # path-shaped artefacts as well, matching the convention in + # `cargo_provider.py` / `npm_provider.py`. + if self.lock_file.is_file(): + self.set_lock_version(version) def set_lock_version(self, version: str) -> None: - 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 - pyproject_toml_content = tomlkit.parse( self.file.read_text(encoding=self._get_encoding()) )