From 53698af637f7ce359748c5f58aad20f27c1e3d4f Mon Sep 17 00:00:00 2001 From: Keith Warno Date: Fri, 9 Oct 2026 16:49:36 -0400 Subject: [PATCH] Skip pip restore for pyproject.toml with uv.lock or poetry.lock RestorePipDependencies claimed any pyproject.toml without a [tool.uv] or [tool.poetry] section. uv and Poetry don't require those sections, so a uv project with a committed uv.lock was handled twice: the uv handler used uv.lock, then the pip handler ran `pip lock .`. On pip < 25.1 that fails and prints a misleading "Error occurred while trying to generate dependencies tree" warning; on newer pip it would also upload a registry-resolved pylock.toml next to uv.lock. Treat a sibling uv.lock or poetry.lock as a sign the manifest belongs to that tool, matching how the uv and Poetry handlers detect their own projects. An existing pylock.toml still wins. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../sca/python/restore_pip_dependencies.py | 8 ++++++ .../python/test_restore_pip_dependencies.py | 25 +++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/cycode/cli/files_collector/sca/python/restore_pip_dependencies.py b/cycode/cli/files_collector/sca/python/restore_pip_dependencies.py index 60423ddd..1110afbf 100644 --- a/cycode/cli/files_collector/sca/python/restore_pip_dependencies.py +++ b/cycode/cli/files_collector/sca/python/restore_pip_dependencies.py @@ -3,6 +3,8 @@ import typer from cycode.cli.files_collector.sca.base_restore_dependencies import BaseRestoreDependencies, build_dep_tree_path +from cycode.cli.files_collector.sca.python.restore_poetry_dependencies import POETRY_LOCK_FILE_NAME +from cycode.cli.files_collector.sca.python.restore_uv_dependencies import UV_LOCK_FILE_NAME from cycode.cli.models import Document from cycode.cli.utils.path_utils import get_file_content from cycode.logger import get_logger @@ -15,6 +17,7 @@ _POETRY_TOOL_SECTION = '[tool.poetry]' _UV_TOOL_SECTION = '[tool.uv]' +_OTHER_TOOL_LOCK_FILE_NAMES = (UV_LOCK_FILE_NAME, POETRY_LOCK_FILE_NAME) def _indicates_plain_pip(pyproject_content: str | None) -> bool: @@ -41,6 +44,11 @@ def is_project(self, document: Document) -> bool: if manifest_dir and (Path(manifest_dir) / PIP_LOCK_FILE_NAME).is_file(): return True + # uv and Poetry don't require a [tool.*] section, so their lockfile is the reliable signal. + # Those handlers already claim this manifest; running `pip lock` on it too is redundant. + if manifest_dir and any((Path(manifest_dir) / name).is_file() for name in _OTHER_TOOL_LOCK_FILE_NAMES): + return False + return _indicates_plain_pip(document.content) def try_restore_dependencies(self, document: Document) -> Document | None: diff --git a/tests/cli/files_collector/sca/python/test_restore_pip_dependencies.py b/tests/cli/files_collector/sca/python/test_restore_pip_dependencies.py index e78bd95d..664ecc43 100644 --- a/tests/cli/files_collector/sca/python/test_restore_pip_dependencies.py +++ b/tests/cli/files_collector/sca/python/test_restore_pip_dependencies.py @@ -52,6 +52,31 @@ def test_pyproject_toml_with_existing_pylock_matches( ) assert restore_pip.is_project(doc) is True + @pytest.mark.parametrize('lock_file_name', ['uv.lock', 'poetry.lock']) + def test_pyproject_toml_with_uv_or_poetry_lock_does_not_match( + self, restore_pip: RestorePipDependencies, tmp_path: Path, lock_file_name: str + ) -> None: + # No [tool.uv] / [tool.poetry] section, but the sibling lockfile shows another tool owns it + content = '[project]\nname = "test"\ndependencies = ["requests"]\n' + (tmp_path / 'pyproject.toml').write_text(content) + (tmp_path / lock_file_name).write_text('version = 1\n') + doc = Document( + str(tmp_path / 'pyproject.toml'), + content, + absolute_path=str(tmp_path / 'pyproject.toml'), + ) + assert restore_pip.is_project(doc) is False + + def test_requirements_txt_with_uv_lock_matches(self, restore_pip: RestorePipDependencies, tmp_path: Path) -> None: + (tmp_path / 'requirements.txt').write_text('requests==2.31.0\n') + (tmp_path / 'uv.lock').write_text('version = 1\n') + doc = Document( + str(tmp_path / 'requirements.txt'), + 'requests==2.31.0\n', + absolute_path=str(tmp_path / 'requirements.txt'), + ) + assert restore_pip.is_project(doc) is True + def test_requirements_txt_matches(self, restore_pip: RestorePipDependencies) -> None: doc = Document('requirements.txt', 'requests==2.31.0\n') assert restore_pip.is_project(doc) is True