Skip to content

Commit 051c6ef

Browse files
authored
fix: change manifest priority for uv workflow (aws#820)
* chore: refactor pyproject flow to use lockfile flow * fix: change manifest priority for uv workflow * fix: pr feedback
1 parent c0163e5 commit 051c6ef

5 files changed

Lines changed: 36 additions & 87 deletions

File tree

aws_lambda_builders/workflows/python_uv/DESIGN.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -109,11 +109,12 @@ The general algorithm for preparing a python package using UV for use on AWS Lam
109109
The workflow uses a smart dispatch system that recognizes actual manifest files:
110110

111111
**Supported Manifests:**
112-
- `pyproject.toml` - Modern Python project manifest (preferred)
112+
- `pyproject.toml` - Modern Python project manifest
113113
- `requirements.txt` - Traditional pip requirements file
114114
- `requirements-*.txt` - Environment-specific variants (dev, prod, test, etc.)
115115

116116
**Smart Lock File Detection:**
117+
- Look for requirements.txt first
117118
- When `pyproject.toml` is the manifest, automatically checks for `uv.lock` in the same directory
118119
- If `uv.lock` exists alongside `pyproject.toml`, uses lock-based build for precise dependencies
119120
- If no `uv.lock`, uses standard pyproject.toml build with UV's lock and export workflow
@@ -258,9 +259,9 @@ CAPABILITY = Capability(
258259
The workflow uses intelligent manifest detection:
259260

260261
**Supported Manifests (in order of preference):**
261-
1. `pyproject.toml` - Modern Python project manifest (preferred)
262-
2. `requirements.txt` - Standard pip format
263-
3. `requirements-*.txt` - Environment-specific variants (dev, test, prod, etc.)
262+
1. `requirements.txt` - Standard pip format
263+
2. `requirements-*.txt` - Environment-specific variants (dev, test, prod, etc.)
264+
3. `pyproject.toml` - Modern Python project manifest
264265

265266
**Smart Lock File Enhancement:**
266267
- When `pyproject.toml` is used, automatically detects `uv.lock` in the same directory

aws_lambda_builders/workflows/python_uv/packager.py

Lines changed: 20 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -323,7 +323,19 @@ def _build_from_lock_file(
323323

324324
# Export lock file to requirements.txt for platform-specific install
325325
temp_requirements = os.path.join(scratch_dir, "lock_requirements.txt")
326-
export_args = ["export", "--format", "requirements-txt", "--no-hashes", "-o", temp_requirements]
326+
export_args = [
327+
"export",
328+
"--format",
329+
"requirements.txt",
330+
"--no-emit-project", # Don't include the project itself, only dependencies
331+
"--no-hashes", # Skip hashes for cleaner output (optional)
332+
"--output-file",
333+
temp_requirements,
334+
# We want to specify the version because `uv export` might default to using a different one
335+
# This is important for dependencies that use different versions depending on python version
336+
"--python",
337+
python_version,
338+
]
327339

328340
rc, stdout, stderr = self._uv_runner._uv.run_uv_command(export_args, cwd=project_dir)
329341
if rc != 0:
@@ -358,86 +370,23 @@ def _build_from_pyproject(
358370
LOG.info("Building from pyproject.toml using UV lock and export")
359371

360372
try:
361-
# Use UV's native workflow: lock -> export -> install
362-
temp_requirements = self._export_pyproject_to_requirements(pyproject_path, scratch_dir, python_version)
363-
364-
if temp_requirements:
365-
self._uv_runner.install_requirements(
366-
requirements_path=temp_requirements,
367-
target_dir=target_dir,
368-
scratch_dir=scratch_dir,
369-
config=config,
370-
python_version=python_version,
371-
platform="linux",
372-
architecture=architecture,
373-
)
374-
else:
375-
LOG.info("No dependencies found in pyproject.toml")
376-
377-
except Exception as e:
378-
raise UvBuildError(reason=f"Failed to build from pyproject.toml: {str(e)}")
379-
380-
def _export_pyproject_to_requirements(
381-
self, pyproject_path: str, scratch_dir: str, python_version: str
382-
) -> Optional[str]:
383-
"""Use UV's native lock and export to convert pyproject.toml to requirements.txt.
384-
385-
This conversion is necessary when pyproject.toml exists without a uv.lock file.
386-
UV's pip install command provides better platform targeting capabilities
387-
(--python-platform) compared to uv sync, which is essential for Lambda's
388-
cross-platform builds (x86_64/ARM64). The workflow is:
389-
1. uv lock: Generate lock file from pyproject.toml
390-
2. uv export: Convert lock file to requirements.txt format
391-
3. Use requirements.txt with uv pip install for platform-specific builds
392-
"""
393-
project_dir = os.path.dirname(pyproject_path)
394-
395-
try:
396-
# Step 1: Create lock file using UV
373+
# Generate lock file from pyproject.toml
397374
LOG.debug("Creating lock file from pyproject.toml")
398375
lock_args = ["lock", "--no-progress"]
399-
400376
if python_version:
401377
lock_args.extend(["--python", python_version])
402378

379+
project_dir = os.path.dirname(pyproject_path)
403380
rc, stdout, stderr = self._uv_runner._uv.run_uv_command(lock_args, cwd=project_dir)
404-
405381
if rc != 0:
406-
LOG.warning(f"UV lock failed: {stderr}")
407-
return None
382+
raise UvBuildError(reason=f"UV lock failed: {stderr}")
408383

409-
# Step 2: Export lock file to requirements.txt format
410-
LOG.debug("Exporting lock file to requirements.txt format")
411-
temp_requirements = os.path.join(scratch_dir, "exported_requirements.txt")
412-
413-
export_args = [
414-
"export",
415-
"--format",
416-
"requirements.txt",
417-
"--no-emit-project", # Don't include the project itself, only dependencies
418-
"--no-header", # Skip comment header
419-
"--no-hashes", # Skip hashes for cleaner output (optional)
420-
"--output-file",
421-
temp_requirements,
422-
]
423-
424-
rc, stdout, stderr = self._uv_runner._uv.run_uv_command(export_args, cwd=project_dir)
425-
426-
if rc != 0:
427-
LOG.warning(f"UV export failed: {stderr}")
428-
return None
429-
430-
# Verify the requirements file was created and has content
431-
if os.path.exists(temp_requirements) and os.path.getsize(temp_requirements) > 0:
432-
LOG.debug(f"Successfully exported dependencies to {temp_requirements}")
433-
return temp_requirements
434-
else:
435-
LOG.info("No dependencies to export from pyproject.toml")
436-
return None
384+
# Reuse lock file build logic
385+
lock_path = os.path.join(project_dir, "uv.lock")
386+
self._build_from_lock_file(lock_path, target_dir, scratch_dir, python_version, architecture, config)
437387

438388
except Exception as e:
439-
LOG.warning(f"Failed to export pyproject.toml using UV native workflow: {e}")
440-
return None
389+
raise UvBuildError(reason=f"Failed to build from pyproject.toml: {str(e)}")
441390

442391
def _build_from_requirements(
443392
self,

aws_lambda_builders/workflows/python_uv/utils.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,20 +37,17 @@ def detect_uv_manifest(source_dir: str) -> Optional[str]:
3737
3838
Note: uv.lock is NOT a manifest - it's a lock file that accompanies pyproject.toml.
3939
UV workflows support these manifest types:
40-
1. pyproject.toml (preferred) - may have accompanying uv.lock
41-
2. requirements.txt and variants - traditional pip-style manifests
40+
1. requirements.txt and variants - traditional pip-style manifests
41+
2. pyproject.toml - may have accompanying uv.lock
42+
43+
We favor requirements.txt because it is possible to have both, but the pyproject could just be project metadata.
4244
4345
Args:
4446
source_dir: Directory to search for manifest files
4547
4648
Returns:
4749
Path to the detected manifest file, or None if not found
4850
"""
49-
# Check for pyproject.toml first (preferred manifest)
50-
pyproject_path = os.path.join(source_dir, "pyproject.toml")
51-
if os.path.isfile(pyproject_path):
52-
return pyproject_path
53-
5451
# Check for requirements.txt variants (in order of preference)
5552
requirements_variants = [
5653
"requirements.txt",
@@ -64,6 +61,10 @@ def detect_uv_manifest(source_dir: str) -> Optional[str]:
6461
if os.path.isfile(requirements_path):
6562
return requirements_path
6663

64+
pyproject_path = os.path.join(source_dir, "pyproject.toml")
65+
if os.path.isfile(pyproject_path):
66+
return pyproject_path
67+
6768
return None
6869

6970

tests/unit/workflows/python_uv/test_packager.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -244,13 +244,11 @@ def test_build_dependencies_pyproject_without_uv_lock(self):
244244
"""Test that pyproject.toml without uv.lock uses standard pyproject build."""
245245
with patch("os.path.basename", return_value="pyproject.toml"), patch(
246246
"os.path.dirname", return_value=os.path.join("path", "to")
247-
), patch("os.path.exists") as mock_exists, patch.object(
248-
self.builder, "_export_pyproject_to_requirements", return_value="/temp/requirements.txt"
249-
):
250-
247+
), patch("os.path.exists") as mock_exists:
251248
# Mock that uv.lock does NOT exist alongside pyproject.toml
252249
mock_exists.return_value = False
253250

251+
self.mock_uv_runner._uv.run_uv_command.return_value = (0, b"", b"")
254252
self.builder.build_dependencies(
255253
artifacts_dir_path="/artifacts",
256254
scratch_dir_path="/scratch",

tests/unit/workflows/python_uv/test_utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ def test_detect_uv_manifest_pyproject_priority(self):
7373

7474
result = detect_uv_manifest(temp_dir)
7575
# pyproject.toml should have priority over requirements.txt
76-
self.assertEqual(result, pyproject_path)
76+
self.assertEqual(result, req_path)
7777

7878
def test_detect_uv_manifest_requirements_variants(self):
7979
with tempfile.TemporaryDirectory() as temp_dir:

0 commit comments

Comments
 (0)