From 1a67ec485367a8c31c9e114d61469817cd962f46 Mon Sep 17 00:00:00 2001 From: Noritada Kobayashi Date: Fri, 10 Jul 2026 01:55:11 +0900 Subject: [PATCH 1/6] fix(python-uv): take into account dependencies on other workspace members MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit fixes an issue where an application created as a member of a uv workspace would fail to build if they depended on other workspace members. The following shows the problematic workspace structure and the error message. ``` workspace root ├── lib │   ├── pyproject.toml │   └── src ├── pyproject.toml ├── sam-app │   ├── __init__.py │   ├── app.py │   ├── pyproject.toml │   ├── samconfig.toml │   └── template.yaml └── uv.lock ``` ``` Build Failed Error: PythonUvBuilder:ResolveDependencies - UV package build failed: Failed to build from pyproject.toml: Lock file operation failed: Failed to install dependencies using uv: UV pip install failed: Using CPython 3.13.0 interpreter at: /path/to/workspace/.venv/bin/python3 error: Distribution not found at: file:///path/to/workspace/sam-app/lib ``` Even though `lib` and `sam-app` are in the same directory level in the workspace, the workflow attempts to install `lib` under `sam-app`. In the workflow, `uv export` outputs a list of dependency packages, which is then passed to `uv pip install` for installation. When doing so, [`uv export` outputs relative paths from the workspace root][1]. Therefore, `uv pip install` must be run from the workspace root, not from the application directory. Additionally, dependencies on other packages within the workspace are exported as editable installations (e.g., `-e ./lib`) by default. When this is passed to `uv pip install`, only the `.pth` (path configuration) file for the package will be installed without the package body. To prevent this, the `--no-editable` option needs to be used. [1]: https://github.com/astral-sh/uv/issues/20238 --- .../workflows/python_uv/packager.py | 12 ++++- .../unit/workflows/python_uv/test_packager.py | 52 ++++++++++++------- 2 files changed, 44 insertions(+), 20 deletions(-) diff --git a/aws_lambda_builders/workflows/python_uv/packager.py b/aws_lambda_builders/workflows/python_uv/packager.py index 291c9c191..2b63f1841 100644 --- a/aws_lambda_builders/workflows/python_uv/packager.py +++ b/aws_lambda_builders/workflows/python_uv/packager.py @@ -332,6 +332,7 @@ def _build_from_lock_file( "--no-emit-project", # Don't include the project itself, only dependencies "--no-hashes", # Skip hashes for cleaner output (optional) "--no-default-groups", # Exclude PEP 735 default groups (e.g. dev/test) from Lambda zips + "--no-editable", # Export editable dependencies as non-editable "--output-file", temp_requirements, # We want to specify the version because `uv export` might default to using a different one @@ -344,6 +345,15 @@ def _build_from_lock_file( if rc != 0: raise LockFileError(reason=f"Failed to export lock file: {stderr}") + # Get the workspace root (or project directory if no workspace is used) + # For packages in the workspace, exported paths are relative to the workspace root, + # regardless of where in the workspace uv export is called + workspace_args = ["workspace", "dir"] + rc, stdout, stderr = self._uv_runner._uv.run_uv_command(workspace_args, cwd=project_dir) + if rc != 0: + raise LockFileError(reason=f"Failed to get workspace root: {stderr}") + workspace_dir = stdout.strip() + # Install with platform targeting self._uv_runner.install_requirements( requirements_path=temp_requirements, @@ -352,7 +362,7 @@ def _build_from_lock_file( config=config, python_version=python_version, platform="linux", - cwd=project_dir, + cwd=workspace_dir, architecture=architecture, ) except LockFileError: diff --git a/tests/unit/workflows/python_uv/test_packager.py b/tests/unit/workflows/python_uv/test_packager.py index 5f9b235e6..276fbaab3 100644 --- a/tests/unit/workflows/python_uv/test_packager.py +++ b/tests/unit/workflows/python_uv/test_packager.py @@ -174,22 +174,30 @@ def test_extract_python_version(self): self.assertIn("Runtime is required", str(context.exception)) def test_build_from_lock_file(self): - # Mock the uv command for export - self.mock_uv_runner._uv.run_uv_command.return_value = (0, b"", b"") - - self.builder._build_from_lock_file( - lock_path="/path/to/uv.lock", - target_dir="/target", - scratch_dir="/scratch", - python_version="3.9", - architecture=X86_64, - config=UvConfig(), - ) + # Mock the uv commands + self.mock_uv_runner._uv.run_uv_command.side_effect = [ + (0, b"", b""), # export + (0, "/workspace\n", b""), # workspace dir + ] + + with patch("os.path.dirname", return_value="/path/to"): + self.builder._build_from_lock_file( + lock_path="/path/to/uv.lock", + target_dir="/target", + scratch_dir="/scratch", + python_version="3.9", + architecture=X86_64, + config=UvConfig(), + ) - # Should call export then install_requirements - self.mock_uv_runner._uv.run_uv_command.assert_called_once() + # Should call export and workspace dir then install_requirements + assert self.mock_uv_runner._uv.run_uv_command.call_count == 2 self.mock_uv_runner.install_requirements.assert_called_once() + # Verify install_requirements is called from workspace root + assert self.mock_uv_runner._uv.run_uv_command.call_args.kwargs["cwd"] == "/path/to" + assert self.mock_uv_runner.install_requirements.call_args.kwargs["cwd"] == "/workspace" + def test_build_from_requirements(self): self.builder._build_from_requirements( requirements_path="/path/to/requirements.txt", @@ -232,15 +240,17 @@ def test_build_dependencies_with_uv_lock_standalone_fails(self): def test_build_dependencies_pyproject_with_uv_lock(self): """Test that pyproject.toml with uv.lock present uses lock-based build.""" - # Mock the uv export command - self.mock_uv_runner._uv.run_uv_command.return_value = (0, b"", b"") + # Mock the uv commands + self.mock_uv_runner._uv.run_uv_command.side_effect = [ + (0, b"", b""), # export + (0, "/workspace\n", b""), # workspace dir + ] with ( patch("os.path.basename", return_value="pyproject.toml"), patch("os.path.dirname", return_value=os.path.join("path", "to")), patch("os.path.exists") as mock_exists, ): - # Mock that uv.lock exists alongside pyproject.toml mock_exists.return_value = True @@ -251,16 +261,20 @@ def test_build_dependencies_pyproject_with_uv_lock(self): architecture=X86_64, ) - # Should use export + install_requirements (for cross-platform support) - self.mock_uv_runner._uv.run_uv_command.assert_called_once() # export + # Should use export + workspace dir + install_requirements (for cross-platform support) + assert self.mock_uv_runner._uv.run_uv_command.call_count == 2 self.mock_uv_runner.install_requirements.assert_called_once() + # Verify install_requirements is called from workspace root + assert self.mock_uv_runner._uv.run_uv_command.call_args.kwargs["cwd"] == "path/to" + assert self.mock_uv_runner.install_requirements.call_args.kwargs["cwd"] == "/workspace" + # Verify it checked for uv.lock in the right location mock_exists.assert_called_with(os.path.join("path", "to", "uv.lock")) # Verify export excludes PEP 735 default dependency-groups (dev/test deps # must not land in Lambda zips). - export_args = self.mock_uv_runner._uv.run_uv_command.call_args[0][0] + export_args = self.mock_uv_runner._uv.run_uv_command.call_args_list[-2][0][0] self.assertIn("--no-default-groups", export_args) def test_build_dependencies_pyproject_without_uv_lock(self): From fa31e78b373bd69194a8517cff55910f0b1c1170 Mon Sep 17 00:00:00 2001 From: Noritada Kobayashi Date: Sat, 5 Sep 2026 15:09:34 +0900 Subject: [PATCH 2/6] fix(python-uv): fix a regression in uv < 0.9.9 --- aws_lambda_builders/workflows/python_uv/packager.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/aws_lambda_builders/workflows/python_uv/packager.py b/aws_lambda_builders/workflows/python_uv/packager.py index 2b63f1841..04ebfb67a 100644 --- a/aws_lambda_builders/workflows/python_uv/packager.py +++ b/aws_lambda_builders/workflows/python_uv/packager.py @@ -332,7 +332,8 @@ def _build_from_lock_file( "--no-emit-project", # Don't include the project itself, only dependencies "--no-hashes", # Skip hashes for cleaner output (optional) "--no-default-groups", # Exclude PEP 735 default groups (e.g. dev/test) from Lambda zips - "--no-editable", # Export editable dependencies as non-editable + # Install package bodies instead of editable .pth links, which break in Lambda zips. + "--no-editable", "--output-file", temp_requirements, # We want to specify the version because `uv export` might default to using a different one @@ -350,9 +351,13 @@ def _build_from_lock_file( # regardless of where in the workspace uv export is called workspace_args = ["workspace", "dir"] rc, stdout, stderr = self._uv_runner._uv.run_uv_command(workspace_args, cwd=project_dir) - if rc != 0: - raise LockFileError(reason=f"Failed to get workspace root: {stderr}") - workspace_dir = stdout.strip() + if rc == 0: + workspace_dir = stdout.strip() + else: + # `uv workspace dir` requires uv >= 0.9.9. Fall back to the project directory, + # which is what that command returns for any non-workspace project anyway. + LOG.debug("Could not determine workspace root, assuming no workspace: %s", stderr) + workspace_dir = project_dir # Install with platform targeting self._uv_runner.install_requirements( From c215596cb25bbfcf92574b089022bfbb55bead99 Mon Sep 17 00:00:00 2001 From: Noritada Kobayashi Date: Mon, 7 Sep 2026 17:13:01 +0900 Subject: [PATCH 3/6] fix(python-uv): update comments to reflect the current state of the code --- aws_lambda_builders/workflows/python_uv/packager.py | 6 +++--- tests/unit/workflows/python_uv/test_packager.py | 5 +++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/aws_lambda_builders/workflows/python_uv/packager.py b/aws_lambda_builders/workflows/python_uv/packager.py index 04ebfb67a..cfc22b800 100644 --- a/aws_lambda_builders/workflows/python_uv/packager.py +++ b/aws_lambda_builders/workflows/python_uv/packager.py @@ -136,9 +136,9 @@ def install_requirements( # Add requirements file args.extend(["-r", requirements_path]) - # Resolve --target to an absolute path: UV runs with cwd set to the project directory, so a - # relative target (e.g. the incremental-build dependencies dir) would otherwise be created - # under the source directory instead of the build root. + # Resolve --target to an absolute path: UV runs from the project or workspace directory, + # so a relative target (e.g. the incremental-build dependencies dir) would otherwise be + # created under the UV's cwd instead of the build root. args.extend(["--target", os.path.abspath(target_dir)]) # Add configuration arguments diff --git a/tests/unit/workflows/python_uv/test_packager.py b/tests/unit/workflows/python_uv/test_packager.py index 276fbaab3..3d43070f5 100644 --- a/tests/unit/workflows/python_uv/test_packager.py +++ b/tests/unit/workflows/python_uv/test_packager.py @@ -126,8 +126,9 @@ def test_install_requirements_success(self): self.assertIn("/path/to/requirements.txt", args_called) def test_install_requirements_resolves_relative_target_to_absolute(self): - # UV runs with cwd=project_dir, so a relative --target must be resolved to an absolute path - # first, otherwise dependencies land under the source dir instead of the build root. + # UV runs from project_dir or workspace_dir, + # so a relative --target must be resolved to an absolute path first, + # otherwise dependencies land under the source dir instead of the build root. self.mock_subprocess_uv.run_uv_command.return_value = (0, "success", "") self.uv_runner.install_requirements( From beccbf569c7acdeb3223d30f50e73bbe77fdb8d4 Mon Sep 17 00:00:00 2001 From: Noritada Kobayashi Date: Mon, 7 Sep 2026 18:02:01 +0900 Subject: [PATCH 4/6] fix(python-uv): ensure that the command being tested is `uv export` --- tests/unit/workflows/python_uv/test_packager.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/unit/workflows/python_uv/test_packager.py b/tests/unit/workflows/python_uv/test_packager.py index 3d43070f5..c70d9f3b4 100644 --- a/tests/unit/workflows/python_uv/test_packager.py +++ b/tests/unit/workflows/python_uv/test_packager.py @@ -275,8 +275,13 @@ def test_build_dependencies_pyproject_with_uv_lock(self): # Verify export excludes PEP 735 default dependency-groups (dev/test deps # must not land in Lambda zips). - export_args = self.mock_uv_runner._uv.run_uv_command.call_args_list[-2][0][0] + export_args = next( + call.args[0] + for call in self.mock_uv_runner._uv.run_uv_command.call_args_list + if call.args[0][0] == "export" + ) self.assertIn("--no-default-groups", export_args) + self.assertIn("--no-editable", export_args) def test_build_dependencies_pyproject_without_uv_lock(self): """Test that pyproject.toml without uv.lock uses standard pyproject build.""" From b813ed82612c4274e0e61c7c597c47e41705fc5c Mon Sep 17 00:00:00 2001 From: Noritada Kobayashi Date: Mon, 7 Sep 2026 20:05:39 +0900 Subject: [PATCH 5/6] fix(python-uv): add integration tests for building with dependencies within workspace --- .../workflows/python_uv/test_python_uv.py | 24 +++++++++++++++++++ .../python_uv/testdata/workspace/app/main.py | 5 ++++ .../testdata/workspace/app/pyproject.toml | 8 +++++++ .../testdata/workspace/lib/pyproject.toml | 8 +++++++ .../lib/src/workspace_lib/__init__.py | 2 ++ .../workspace/lib/src/workspace_lib/py.typed | 0 .../testdata/workspace/pyproject.toml | 2 ++ 7 files changed, 49 insertions(+) create mode 100644 tests/integration/workflows/python_uv/testdata/workspace/app/main.py create mode 100644 tests/integration/workflows/python_uv/testdata/workspace/app/pyproject.toml create mode 100644 tests/integration/workflows/python_uv/testdata/workspace/lib/pyproject.toml create mode 100644 tests/integration/workflows/python_uv/testdata/workspace/lib/src/workspace_lib/__init__.py create mode 100644 tests/integration/workflows/python_uv/testdata/workspace/lib/src/workspace_lib/py.typed create mode 100644 tests/integration/workflows/python_uv/testdata/workspace/pyproject.toml diff --git a/tests/integration/workflows/python_uv/test_python_uv.py b/tests/integration/workflows/python_uv/test_python_uv.py index 81af2adfc..e1bc20f98 100644 --- a/tests/integration/workflows/python_uv/test_python_uv.py +++ b/tests/integration/workflows/python_uv/test_python_uv.py @@ -297,3 +297,27 @@ def test_workflow_builds_numpy_with_pyproject(self): finally: shutil.rmtree(temp_source_dir) + + @skipIf(which("uv") is None, "uv not available") + def test_workflow_builds_with_dependencies_within_workspace(self): + with tempfile.TemporaryDirectory() as workspace_dir: + shutil.copytree(os.path.join(self.TEST_DATA_FOLDER, "workspace"), workspace_dir, dirs_exist_ok=True) + source_dir = os.path.join(workspace_dir, "app") + builder = LambdaBuilder(language="python", dependency_manager="uv", application_framework=None) + builder.build( + source_dir, + self.artifacts_dir, + self.scratch_dir, + os.path.join(source_dir, "pyproject.toml"), + runtime=f"python{sys.version_info.major}.{sys.version_info.minor}", + experimental_flags=self.experimental_flags, + ) + + self.assertTrue(os.path.isfile(os.path.join(workspace_dir, "uv.lock"))) + self.assertFalse(os.path.exists(os.path.join(source_dir, "uv.lock"))) + for filename in ("__init__.py", "py.typed"): + installed = pathlib.Path(self.artifacts_dir, "workspace_lib", filename) + original = pathlib.Path(workspace_dir, "lib", "src", "workspace_lib", filename) + self.assertEqual(installed.read_bytes(), original.read_bytes()) + self.assertTrue(os.path.isdir(os.path.join(self.artifacts_dir, "workspace_lib-0.1.0.dist-info"))) + self.assertEqual(list(pathlib.Path(self.artifacts_dir).rglob("*.pth")), []) diff --git a/tests/integration/workflows/python_uv/testdata/workspace/app/main.py b/tests/integration/workflows/python_uv/testdata/workspace/app/main.py new file mode 100644 index 000000000..16486e1d4 --- /dev/null +++ b/tests/integration/workflows/python_uv/testdata/workspace/app/main.py @@ -0,0 +1,5 @@ +from workspace_lib import message + + +def handler(event, context): + return message() diff --git a/tests/integration/workflows/python_uv/testdata/workspace/app/pyproject.toml b/tests/integration/workflows/python_uv/testdata/workspace/app/pyproject.toml new file mode 100644 index 000000000..37a04f45c --- /dev/null +++ b/tests/integration/workflows/python_uv/testdata/workspace/app/pyproject.toml @@ -0,0 +1,8 @@ +[project] +name = "workspace-app" +version = "0.1.0" +requires-python = ">=3.9" +dependencies = ["workspace-lib"] + +[tool.uv.sources] +workspace-lib = { workspace = true } diff --git a/tests/integration/workflows/python_uv/testdata/workspace/lib/pyproject.toml b/tests/integration/workflows/python_uv/testdata/workspace/lib/pyproject.toml new file mode 100644 index 000000000..aa0e23e02 --- /dev/null +++ b/tests/integration/workflows/python_uv/testdata/workspace/lib/pyproject.toml @@ -0,0 +1,8 @@ +[project] +name = "workspace-lib" +version = "0.1.0" +requires-python = ">=3.9" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" diff --git a/tests/integration/workflows/python_uv/testdata/workspace/lib/src/workspace_lib/__init__.py b/tests/integration/workflows/python_uv/testdata/workspace/lib/src/workspace_lib/__init__.py new file mode 100644 index 000000000..2d4f6400a --- /dev/null +++ b/tests/integration/workflows/python_uv/testdata/workspace/lib/src/workspace_lib/__init__.py @@ -0,0 +1,2 @@ +def message(): + return "Hello from the workspace dependency" diff --git a/tests/integration/workflows/python_uv/testdata/workspace/lib/src/workspace_lib/py.typed b/tests/integration/workflows/python_uv/testdata/workspace/lib/src/workspace_lib/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/tests/integration/workflows/python_uv/testdata/workspace/pyproject.toml b/tests/integration/workflows/python_uv/testdata/workspace/pyproject.toml new file mode 100644 index 000000000..d51ceca70 --- /dev/null +++ b/tests/integration/workflows/python_uv/testdata/workspace/pyproject.toml @@ -0,0 +1,2 @@ +[tool.uv.workspace] +members = ["app", "lib"] From 85b30bd05400b4237dd681640284bad7772a11d4 Mon Sep 17 00:00:00 2001 From: Noritada Kobayashi Date: Mon, 7 Sep 2026 20:27:44 +0900 Subject: [PATCH 6/6] fix(python-uv): fix an assertion failure on Windows --- tests/unit/workflows/python_uv/test_packager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/workflows/python_uv/test_packager.py b/tests/unit/workflows/python_uv/test_packager.py index c70d9f3b4..dbdc76866 100644 --- a/tests/unit/workflows/python_uv/test_packager.py +++ b/tests/unit/workflows/python_uv/test_packager.py @@ -267,7 +267,7 @@ def test_build_dependencies_pyproject_with_uv_lock(self): self.mock_uv_runner.install_requirements.assert_called_once() # Verify install_requirements is called from workspace root - assert self.mock_uv_runner._uv.run_uv_command.call_args.kwargs["cwd"] == "path/to" + assert self.mock_uv_runner._uv.run_uv_command.call_args.kwargs["cwd"] == os.path.join("path", "to") assert self.mock_uv_runner.install_requirements.call_args.kwargs["cwd"] == "/workspace" # Verify it checked for uv.lock in the right location